/** * WordPress Link Template Functions * * @package WordPress * @subpackage Template */ /** * Displays the permalink for the current post. * * @since 1.2.0 * @since 4.4.0 Added the `$post` parameter. * * @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`. */ function the_permalink( $post = 0 ) { /** * Filters the display of the permalink for the current post. * * @since 1.5.0 * @since 4.4.0 Added the `$post` parameter. * * @param string $permalink The permalink for the current post. * @param int|WP_Post $post Post ID, WP_Post object, or 0. Default 0. */ echo esc_url( apply_filters( 'the_permalink', get_permalink( $post ), $post ) ); } /** * Retrieves a trailing-slashed string if the site is set for adding trailing slashes. * * Conditionally adds a trailing slash if the permalink structure has a trailing * slash, strips the trailing slash if not. The string is passed through the * {@see 'user_trailingslashit'} filter. Will remove trailing slash from string, if * site is not set to have them. * * @since 2.2.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param string $url URL with or without a trailing slash. * @param string $type_of_url Optional. The type of URL being considered (e.g. single, category, etc) * for use in the filter. Default empty string. * @return string The URL with the trailing slash appended or stripped. */ function user_trailingslashit( $url, $type_of_url = '' ) { global $wp_rewrite; if ( $wp_rewrite->use_trailing_slashes ) { $url = trailingslashit( $url ); } else { $url = untrailingslashit( $url ); } /** * Filters the trailing-slashed string, depending on whether the site is set to use trailing slashes. * * @since 2.2.0 * * @param string $url URL with or without a trailing slash. * @param string $type_of_url The type of URL being considered. Accepts 'single', 'single_trackback', * 'single_feed', 'single_paged', 'commentpaged', 'paged', 'home', 'feed', * 'category', 'page', 'year', 'month', 'day', 'post_type_archive'. */ return apply_filters( 'user_trailingslashit', $url, $type_of_url ); } /** * Displays the permalink anchor for the current post. * * The permalink mode title will use the post title for the 'a' element 'id' * attribute. The id mode uses 'post-' with the post ID for the 'id' attribute. * * @since 0.71 * * @param string $mode Optional. Permalink mode. Accepts 'title' or 'id'. Default 'id'. */ function permalink_anchor( $mode = 'id' ) { $post = get_post(); switch ( strtolower( $mode ) ) { case 'title': $title = sanitize_title( $post->post_title ) . '-' . $post->ID; echo ''; break; case 'id': default: echo ''; break; } } /** * Determine whether post should always use a plain permalink structure. * * @since 5.7.0 * * @param WP_Post|int|null $post Optional. Post ID or post object. Defaults to global $post. * @param bool|null $sample Optional. Whether to force consideration based on sample links. * If omitted, a sample link is generated if a post object is passed * with the filter property set to 'sample'. * @return bool Whether to use a plain permalink structure. */ function wp_force_plain_post_permalink( $post = null, $sample = null ) { if ( null === $sample && is_object( $post ) && isset( $post->filter ) && 'sample' === $post->filter ) { $sample = true; } else { $post = get_post( $post ); $sample = null !== $sample ? $sample : false; } if ( ! $post ) { return true; } $post_status_obj = get_post_status_object( get_post_status( $post ) ); $post_type_obj = get_post_type_object( get_post_type( $post ) ); if ( ! $post_status_obj || ! $post_type_obj ) { return true; } if ( // Publicly viewable links never have plain permalinks. is_post_status_viewable( $post_status_obj ) || ( // Private posts don't have plain permalinks if the user can read them. $post_status_obj->private && current_user_can( 'read_post', $post->ID ) ) || // Protected posts don't have plain links if getting a sample URL. ( $post_status_obj->protected && $sample ) ) { return false; } return true; } /** * Retrieves the full permalink for the current post or post ID. * * This function is an alias for get_permalink(). * * @since 3.9.0 * * @see get_permalink() * * @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`. * @param bool $leavename Optional. Whether to keep post name or page name. Default false. * @return string|false The permalink URL. False if the post does not exist. */ function get_the_permalink( $post = 0, $leavename = false ) { return get_permalink( $post, $leavename ); } /** * Retrieves the full permalink for the current post or post ID. * * @since 1.0.0 * * @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`. * @param bool $leavename Optional. Whether to keep post name or page name. Default false. * @return string|false The permalink URL. False if the post does not exist. */ function get_permalink( $post = 0, $leavename = false ) { $rewritecode = array( '%year%', '%monthnum%', '%day%', '%hour%', '%minute%', '%second%', $leavename ? '' : '%postname%', '%post_id%', '%category%', '%author%', $leavename ? '' : '%pagename%', ); if ( is_object( $post ) && isset( $post->filter ) && 'sample' === $post->filter ) { $sample = true; } else { $post = get_post( $post ); $sample = false; } if ( empty( $post->ID ) ) { return false; } if ( 'page' === $post->post_type ) { return get_page_link( $post, $leavename, $sample ); } elseif ( 'attachment' === $post->post_type ) { return get_attachment_link( $post, $leavename ); } elseif ( in_array( $post->post_type, get_post_types( array( '_builtin' => false ) ), true ) ) { return get_post_permalink( $post, $leavename, $sample ); } $permalink = get_option( 'permalink_structure' ); /** * Filters the permalink structure for a post before token replacement occurs. * * Only applies to posts with post_type of 'post'. * * @since 3.0.0 * * @param string $permalink The site's permalink structure. * @param WP_Post $post The post in question. * @param bool $leavename Whether to keep the post name. */ $permalink = apply_filters( 'pre_post_link', $permalink, $post, $leavename ); if ( $permalink && ! wp_force_plain_post_permalink( $post ) ) { $category = ''; if ( str_contains( $permalink, '%category%' ) ) { $cats = get_the_category( $post->ID ); if ( $cats ) { $cats = wp_list_sort( $cats, array( 'term_id' => 'ASC', ) ); /** * Filters the category that gets used in the %category% permalink token. * * @since 3.5.0 * * @param WP_Term $cat The category to use in the permalink. * @param array $cats Array of all categories (WP_Term objects) associated with the post. * @param WP_Post $post The post in question. */ $category_object = apply_filters( 'post_link_category', $cats[0], $cats, $post ); $category_object = get_term( $category_object, 'category' ); $category = $category_object->slug; if ( $category_object->parent ) { $category = get_category_parents( $category_object->parent, false, '/', true ) . $category; } } /* * Show default category in permalinks, * without having to assign it explicitly. */ if ( empty( $category ) ) { $default_category = get_term( get_option( 'default_category' ), 'category' ); if ( $default_category && ! is_wp_error( $default_category ) ) { $category = $default_category->slug; } } } $author = ''; if ( str_contains( $permalink, '%author%' ) ) { $authordata = get_userdata( $post->post_author ); $author = $authordata->user_nicename; } /* * This is not an API call because the permalink is based on the stored post_date value, * which should be parsed as local time regardless of the default PHP timezone. */ $date = explode( ' ', str_replace( array( '-', ':' ), ' ', $post->post_date ) ); $rewritereplace = array( $date[0], $date[1], $date[2], $date[3], $date[4], $date[5], $post->post_name, $post->ID, $category, $author, $post->post_name, ); $permalink = home_url( str_replace( $rewritecode, $rewritereplace, $permalink ) ); $permalink = user_trailingslashit( $permalink, 'single' ); } else { // If they're not using the fancy permalink option. $permalink = home_url( '?p=' . $post->ID ); } /** * Filters the permalink for a post. * * Only applies to posts with post_type of 'post'. * * @since 1.5.0 * * @param string $permalink The post's permalink. * @param WP_Post $post The post in question. * @param bool $leavename Whether to keep the post name. */ return apply_filters( 'post_link', $permalink, $post, $leavename ); } /** * Retrieves the permalink for a post of a custom post type. * * @since 3.0.0 * @since 6.1.0 Returns false if the post does not exist. * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`. * @param bool $leavename Optional. Whether to keep post name. Default false. * @param bool $sample Optional. Is it a sample permalink. Default false. * @return string|false The post permalink URL. False if the post does not exist. */ function get_post_permalink( $post = 0, $leavename = false, $sample = false ) { global $wp_rewrite; $post = get_post( $post ); if ( ! $post ) { return false; } $post_link = $wp_rewrite->get_extra_permastruct( $post->post_type ); $slug = $post->post_name; $force_plain_link = wp_force_plain_post_permalink( $post ); $post_type = get_post_type_object( $post->post_type ); if ( $post_type->hierarchical ) { $slug = get_page_uri( $post ); } if ( ! empty( $post_link ) && ( ! $force_plain_link || $sample ) ) { if ( ! $leavename ) { $post_link = str_replace( "%$post->post_type%", $slug, $post_link ); } $post_link = home_url( user_trailingslashit( $post_link ) ); } else { if ( $post_type->query_var && ( isset( $post->post_status ) && ! $force_plain_link ) ) { $post_link = add_query_arg( $post_type->query_var, $slug, '' ); } else { $post_link = add_query_arg( array( 'post_type' => $post->post_type, 'p' => $post->ID, ), '' ); } $post_link = home_url( $post_link ); } /** * Filters the permalink for a post of a custom post type. * * @since 3.0.0 * * @param string $post_link The post's permalink. * @param WP_Post $post The post in question. * @param bool $leavename Whether to keep the post name. * @param bool $sample Is it a sample permalink. */ return apply_filters( 'post_type_link', $post_link, $post, $leavename, $sample ); } /** * Retrieves the permalink for the current page or page ID. * * Respects page_on_front. Use this one. * * @since 1.5.0 * * @param int|WP_Post $post Optional. Post ID or object. Default uses the global `$post`. * @param bool $leavename Optional. Whether to keep the page name. Default false. * @param bool $sample Optional. Whether it should be treated as a sample permalink. * Default false. * @return string The page permalink. */ function get_page_link( $post = false, $leavename = false, $sample = false ) { $post = get_post( $post ); if ( 'page' === get_option( 'show_on_front' ) && get_option( 'page_on_front' ) == $post->ID ) { $link = home_url( '/' ); } else { $link = _get_page_link( $post, $leavename, $sample ); } /** * Filters the permalink for a page. * * @since 1.5.0 * * @param string $link The page's permalink. * @param int $post_id The ID of the page. * @param bool $sample Is it a sample permalink. */ return apply_filters( 'page_link', $link, $post->ID, $sample ); } /** * Retrieves the page permalink. * * Ignores page_on_front. Internal use only. * * @since 2.1.0 * @access private * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param int|WP_Post $post Optional. Post ID or object. Default uses the global `$post`. * @param bool $leavename Optional. Whether to keep the page name. Default false. * @param bool $sample Optional. Whether it should be treated as a sample permalink. * Default false. * @return string The page permalink. */ function _get_page_link( $post = false, $leavename = false, $sample = false ) { global $wp_rewrite; $post = get_post( $post ); $force_plain_link = wp_force_plain_post_permalink( $post ); $link = $wp_rewrite->get_page_permastruct(); if ( ! empty( $link ) && ( ( isset( $post->post_status ) && ! $force_plain_link ) || $sample ) ) { if ( ! $leavename ) { $link = str_replace( '%pagename%', get_page_uri( $post ), $link ); } $link = home_url( $link ); $link = user_trailingslashit( $link, 'page' ); } else { $link = home_url( '?page_id=' . $post->ID ); } /** * Filters the permalink for a non-page_on_front page. * * @since 2.1.0 * * @param string $link The page's permalink. * @param int $post_id The ID of the page. */ return apply_filters( '_get_page_link', $link, $post->ID ); } /** * Retrieves the permalink for an attachment. * * This can be used in the WordPress Loop or outside of it. * * @since 2.0.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param int|object $post Optional. Post ID or object. Default uses the global `$post`. * @param bool $leavename Optional. Whether to keep the page name. Default false. * @return string The attachment permalink. */ function get_attachment_link( $post = null, $leavename = false ) { global $wp_rewrite; $link = false; $post = get_post( $post ); $force_plain_link = wp_force_plain_post_permalink( $post ); $parent_id = $post->post_parent; $parent = $parent_id ? get_post( $parent_id ) : false; $parent_valid = true; // Default for no parent. if ( $parent_id && ( $post->post_parent === $post->ID || ! $parent || ! is_post_type_viewable( get_post_type( $parent ) ) ) ) { // Post is either its own parent or parent post unavailable. $parent_valid = false; } if ( $force_plain_link || ! $parent_valid ) { $link = false; } elseif ( $wp_rewrite->using_permalinks() && $parent ) { if ( 'page' === $parent->post_type ) { $parentlink = _get_page_link( $post->post_parent ); // Ignores page_on_front. } else { $parentlink = get_permalink( $post->post_parent ); } if ( is_numeric( $post->post_name ) || str_contains( get_option( 'permalink_structure' ), '%category%' ) ) { $name = 'attachment/' . $post->post_name; // // is paged so we use the explicit attachment marker. } else { $name = $post->post_name; } if ( ! str_contains( $parentlink, '?' ) ) { $link = user_trailingslashit( trailingslashit( $parentlink ) . '%postname%' ); } if ( ! $leavename ) { $link = str_replace( '%postname%', $name, $link ); } } elseif ( $wp_rewrite->using_permalinks() && ! $leavename ) { $link = home_url( user_trailingslashit( $post->post_name ) ); } if ( ! $link ) { $link = home_url( '/?attachment_id=' . $post->ID ); } /** * Filters the permalink for an attachment. * * @since 2.0.0 * @since 5.6.0 Providing an empty string will now disable * the view attachment page link on the media modal. * * @param string $link The attachment's permalink. * @param int $post_id Attachment ID. */ return apply_filters( 'attachment_link', $link, $post->ID ); } /** * Retrieves the permalink for the year archives. * * @since 1.5.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param int|false $year Integer of year. False for current year. * @return string The permalink for the specified year archive. */ function get_year_link( $year ) { global $wp_rewrite; if ( ! $year ) { $year = current_time( 'Y' ); } $yearlink = $wp_rewrite->get_year_permastruct(); if ( ! empty( $yearlink ) ) { $yearlink = str_replace( '%year%', $year, $yearlink ); $yearlink = home_url( user_trailingslashit( $yearlink, 'year' ) ); } else { $yearlink = home_url( '?m=' . $year ); } /** * Filters the year archive permalink. * * @since 1.5.0 * * @param string $yearlink Permalink for the year archive. * @param int $year Year for the archive. */ return apply_filters( 'year_link', $yearlink, $year ); } /** * Retrieves the permalink for the month archives with year. * * @since 1.0.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param int|false $year Integer of year. False for current year. * @param int|false $month Integer of month. False for current month. * @return string The permalink for the specified month and year archive. */ function get_month_link( $year, $month ) { global $wp_rewrite; if ( ! $year ) { $year = current_time( 'Y' ); } if ( ! $month ) { $month = current_time( 'm' ); } $monthlink = $wp_rewrite->get_month_permastruct(); if ( ! empty( $monthlink ) ) { $monthlink = str_replace( '%year%', $year, $monthlink ); $monthlink = str_replace( '%monthnum%', zeroise( (int) $month, 2 ), $monthlink ); $monthlink = home_url( user_trailingslashit( $monthlink, 'month' ) ); } else { $monthlink = home_url( '?m=' . $year . zeroise( $month, 2 ) ); } /** * Filters the month archive permalink. * * @since 1.5.0 * * @param string $monthlink Permalink for the month archive. * @param int $year Year for the archive. * @param int $month The month for the archive. */ return apply_filters( 'month_link', $monthlink, $year, $month ); } /** * Retrieves the permalink for the day archives with year and month. * * @since 1.0.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param int|false $year Integer of year. False for current year. * @param int|false $month Integer of month. False for current month. * @param int|false $day Integer of day. False for current day. * @return string The permalink for the specified day, month, and year archive. */ function get_day_link( $year, $month, $day ) { global $wp_rewrite; if ( ! $year ) { $year = current_time( 'Y' ); } if ( ! $month ) { $month = current_time( 'm' ); } if ( ! $day ) { $day = current_time( 'j' ); } $daylink = $wp_rewrite->get_day_permastruct(); if ( ! empty( $daylink ) ) { $daylink = str_replace( '%year%', $year, $daylink ); $daylink = str_replace( '%monthnum%', zeroise( (int) $month, 2 ), $daylink ); $daylink = str_replace( '%day%', zeroise( (int) $day, 2 ), $daylink ); $daylink = home_url( user_trailingslashit( $daylink, 'day' ) ); } else { $daylink = home_url( '?m=' . $year . zeroise( $month, 2 ) . zeroise( $day, 2 ) ); } /** * Filters the day archive permalink. * * @since 1.5.0 * * @param string $daylink Permalink for the day archive. * @param int $year Year for the archive. * @param int $month Month for the archive. * @param int $day The day for the archive. */ return apply_filters( 'day_link', $daylink, $year, $month, $day ); } /** * Displays the permalink for the feed type. * * @since 3.0.0 * * @param string $anchor The link's anchor text. * @param string $feed Optional. Feed type. Possible values include 'rss2', 'atom'. * Default is the value of get_default_feed(). */ function the_feed_link( $anchor, $feed = '' ) { $link = '' . $anchor . ''; /** * Filters the feed link anchor tag. * * @since 3.0.0 * * @param string $link The complete anchor tag for a feed link. * @param string $feed The feed type. Possible values include 'rss2', 'atom', * or an empty string for the default feed type. */ echo apply_filters( 'the_feed_link', $link, $feed ); } /** * Retrieves the permalink for the feed type. * * @since 1.5.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param string $feed Optional. Feed type. Possible values include 'rss2', 'atom'. * Default is the value of get_default_feed(). * @return string The feed permalink. */ function get_feed_link( $feed = '' ) { global $wp_rewrite; $permalink = $wp_rewrite->get_feed_permastruct(); if ( $permalink ) { if ( str_contains( $feed, 'comments_' ) ) { $feed = str_replace( 'comments_', '', $feed ); $permalink = $wp_rewrite->get_comment_feed_permastruct(); } if ( get_default_feed() == $feed ) { $feed = ''; } $permalink = str_replace( '%feed%', $feed, $permalink ); $permalink = preg_replace( '#/+#', '/', "/$permalink" ); $output = home_url( user_trailingslashit( $permalink, 'feed' ) ); } else { if ( empty( $feed ) ) { $feed = get_default_feed(); } if ( str_contains( $feed, 'comments_' ) ) { $feed = str_replace( 'comments_', 'comments-', $feed ); } $output = home_url( "?feed={$feed}" ); } /** * Filters the feed type permalink. * * @since 1.5.0 * * @param string $output The feed permalink. * @param string $feed The feed type. Possible values include 'rss2', 'atom', * or an empty string for the default feed type. */ return apply_filters( 'feed_link', $output, $feed ); } /** * Retrieves the permalink for the post comments feed. * * @since 2.2.0 * * @param int $post_id Optional. Post ID. Default is the ID of the global `$post`. * @param string $feed Optional. Feed type. Possible values include 'rss2', 'atom'. * Default is the value of get_default_feed(). * @return string The permalink for the comments feed for the given post on success, empty string on failure. */ function get_post_comments_feed_link( $post_id = 0, $feed = '' ) { $post_id = absint( $post_id ); if ( ! $post_id ) { $post_id = get_the_ID(); } if ( empty( $feed ) ) { $feed = get_default_feed(); } $post = get_post( $post_id ); // Bail out if the post does not exist. if ( ! $post instanceof WP_Post ) { return ''; } $unattached = 'attachment' === $post->post_type && 0 === (int) $post->post_parent; if ( get_option( 'permalink_structure' ) ) { if ( 'page' === get_option( 'show_on_front' ) && get_option( 'page_on_front' ) == $post_id ) { $url = _get_page_link( $post_id ); } else { $url = get_permalink( $post_id ); } if ( $unattached ) { $url = home_url( '/feed/' ); if ( get_default_feed() !== $feed ) { $url .= "$feed/"; } $url = add_query_arg( 'attachment_id', $post_id, $url ); } else { $url = trailingslashit( $url ) . 'feed'; if ( get_default_feed() != $feed ) { $url .= "/$feed"; } $url = user_trailingslashit( $url, 'single_feed' ); } } else { if ( $unattached ) { $url = add_query_arg( array( 'feed' => $feed, 'attachment_id' => $post_id, ), home_url( '/' ) ); } elseif ( 'page' === $post->post_type ) { $url = add_query_arg( array( 'feed' => $feed, 'page_id' => $post_id, ), home_url( '/' ) ); } else { $url = add_query_arg( array( 'feed' => $feed, 'p' => $post_id, ), home_url( '/' ) ); } } /** * Filters the post comments feed permalink. * * @since 1.5.1 * * @param string $url Post comments feed permalink. */ return apply_filters( 'post_comments_feed_link', $url ); } /** * Displays the comment feed link for a post. * * Prints out the comment feed link for a post. Link text is placed in the * anchor. If no link text is specified, default text is used. If no post ID is * specified, the current post is used. * * @since 2.5.0 * * @param string $link_text Optional. Descriptive link text. Default 'Comments Feed'. * @param int $post_id Optional. Post ID. Default is the ID of the global `$post`. * @param string $feed Optional. Feed type. Possible values include 'rss2', 'atom'. * Default is the value of get_default_feed(). */ function post_comments_feed_link( $link_text = '', $post_id = '', $feed = '' ) { $url = get_post_comments_feed_link( $post_id, $feed ); if ( empty( $link_text ) ) { $link_text = __( 'Comments Feed' ); } $link = '' . $link_text . ''; /** * Filters the post comment feed link anchor tag. * * @since 2.8.0 * * @param string $link The complete anchor tag for the comment feed link. * @param int $post_id Post ID. * @param string $feed The feed type. Possible values include 'rss2', 'atom', * or an empty string for the default feed type. */ echo apply_filters( 'post_comments_feed_link_html', $link, $post_id, $feed ); } /** * Retrieves the feed link for a given author. * * Returns a link to the feed for all posts by a given author. A specific feed * can be requested or left blank to get the default feed. * * @since 2.5.0 * * @param int $author_id Author ID. * @param string $feed Optional. Feed type. Possible values include 'rss2', 'atom'. * Default is the value of get_default_feed(). * @return string Link to the feed for the author specified by $author_id. */ function get_author_feed_link( $author_id, $feed = '' ) { $author_id = (int) $author_id; $permalink_structure = get_option( 'permalink_structure' ); if ( empty( $feed ) ) { $feed = get_default_feed(); } if ( ! $permalink_structure ) { $link = home_url( "?feed=$feed&author=" . $author_id ); } else { $link = get_author_posts_url( $author_id ); if ( get_default_feed() == $feed ) { $feed_link = 'feed'; } else { $feed_link = "feed/$feed"; } $link = trailingslashit( $link ) . user_trailingslashit( $feed_link, 'feed' ); } /** * Filters the feed link for a given author. * * @since 1.5.1 * * @param string $link The author feed link. * @param string $feed Feed type. Possible values include 'rss2', 'atom'. */ $link = apply_filters( 'author_feed_link', $link, $feed ); return $link; } /** * Retrieves the feed link for a category. * * Returns a link to the feed for all posts in a given category. A specific feed * can be requested or left blank to get the default feed. * * @since 2.5.0 * * @param int|WP_Term|object $cat The ID or category object whose feed link will be retrieved. * @param string $feed Optional. Feed type. Possible values include 'rss2', 'atom'. * Default is the value of get_default_feed(). * @return string Link to the feed for the category specified by `$cat`. */ function get_category_feed_link( $cat, $feed = '' ) { return get_term_feed_link( $cat, 'category', $feed ); } /** * Retrieves the feed link for a term. * * Returns a link to the feed for all posts in a given term. A specific feed * can be requested or left blank to get the default feed. * * @since 3.0.0 * * @param int|WP_Term|object $term The ID or term object whose feed link will be retrieved. * @param string $taxonomy Optional. Taxonomy of `$term_id`. * @param string $feed Optional. Feed type. Possible values include 'rss2', 'atom'. * Default is the value of get_default_feed(). * @return string|false Link to the feed for the term specified by `$term` and `$taxonomy`. */ function get_term_feed_link( $term, $taxonomy = '', $feed = '' ) { if ( ! is_object( $term ) ) { $term = (int) $term; } $term = get_term( $term, $taxonomy ); if ( empty( $term ) || is_wp_error( $term ) ) { return false; } $taxonomy = $term->taxonomy; if ( empty( $feed ) ) { $feed = get_default_feed(); } $permalink_structure = get_option( 'permalink_structure' ); if ( ! $permalink_structure ) { if ( 'category' === $taxonomy ) { $link = home_url( "?feed=$feed&cat=$term->term_id" ); } elseif ( 'post_tag' === $taxonomy ) { $link = home_url( "?feed=$feed&tag=$term->slug" ); } else { $t = get_taxonomy( $taxonomy ); $link = home_url( "?feed=$feed&$t->query_var=$term->slug" ); } } else { $link = get_term_link( $term, $term->taxonomy ); if ( get_default_feed() == $feed ) { $feed_link = 'feed'; } else { $feed_link = "feed/$feed"; } $link = trailingslashit( $link ) . user_trailingslashit( $feed_link, 'feed' ); } if ( 'category' === $taxonomy ) { /** * Filters the category feed link. * * @since 1.5.1 * * @param string $link The category feed link. * @param string $feed Feed type. Possible values include 'rss2', 'atom'. */ $link = apply_filters( 'category_feed_link', $link, $feed ); } elseif ( 'post_tag' === $taxonomy ) { /** * Filters the post tag feed link. * * @since 2.3.0 * * @param string $link The tag feed link. * @param string $feed Feed type. Possible values include 'rss2', 'atom'. */ $link = apply_filters( 'tag_feed_link', $link, $feed ); } else { /** * Filters the feed link for a taxonomy other than 'category' or 'post_tag'. * * @since 3.0.0 * * @param string $link The taxonomy feed link. * @param string $feed Feed type. Possible values include 'rss2', 'atom'. * @param string $taxonomy The taxonomy name. */ $link = apply_filters( 'taxonomy_feed_link', $link, $feed, $taxonomy ); } return $link; } /** * Retrieves the permalink for a tag feed. * * @since 2.3.0 * * @param int|WP_Term|object $tag The ID or term object whose feed link will be retrieved. * @param string $feed Optional. Feed type. Possible values include 'rss2', 'atom'. * Default is the value of get_default_feed(). * @return string The feed permalink for the given tag. */ function get_tag_feed_link( $tag, $feed = '' ) { return get_term_feed_link( $tag, 'post_tag', $feed ); } /** * Retrieves the edit link for a tag. * * @since 2.7.0 * * @param int|WP_Term|object $tag The ID or term object whose edit link will be retrieved. * @param string $taxonomy Optional. Taxonomy slug. Default 'post_tag'. * @return string The edit tag link URL for the given tag. */ function get_edit_tag_link( $tag, $taxonomy = 'post_tag' ) { /** * Filters the edit link for a tag (or term in another taxonomy). * * @since 2.7.0 * * @param string $link The term edit link. */ return apply_filters( 'get_edit_tag_link', get_edit_term_link( $tag, $taxonomy ) ); } /** * Displays or retrieves the edit link for a tag with formatting. * * @since 2.7.0 * * @param string $link Optional. Anchor text. If empty, default is 'Edit This'. Default empty. * @param string $before Optional. Display before edit link. Default empty. * @param string $after Optional. Display after edit link. Default empty. * @param WP_Term $tag Optional. Term object. If null, the queried object will be inspected. * Default null. */ function edit_tag_link( $link = '', $before = '', $after = '', $tag = null ) { $link = edit_term_link( $link, '', '', $tag, false ); /** * Filters the anchor tag for the edit link for a tag (or term in another taxonomy). * * @since 2.7.0 * * @param string $link The anchor tag for the edit link. */ echo $before . apply_filters( 'edit_tag_link', $link ) . $after; } /** * Retrieves the URL for editing a given term. * * @since 3.1.0 * @since 4.5.0 The `$taxonomy` parameter was made optional. * * @param int|WP_Term|object $term The ID or term object whose edit link will be retrieved. * @param string $taxonomy Optional. Taxonomy. Defaults to the taxonomy of the term identified * by `$term`. * @param string $object_type Optional. The object type. Used to highlight the proper post type * menu on the linked page. Defaults to the first object_type associated * with the taxonomy. * @return string|null The edit term link URL for the given term, or null on failure. */ function get_edit_term_link( $term, $taxonomy = '', $object_type = '' ) { $term = get_term( $term, $taxonomy ); if ( ! $term || is_wp_error( $term ) ) { return; } $tax = get_taxonomy( $term->taxonomy ); $term_id = $term->term_id; if ( ! $tax || ! current_user_can( 'edit_term', $term_id ) ) { return; } $args = array( 'taxonomy' => $taxonomy, 'tag_ID' => $term_id, ); if ( $object_type ) { $args['post_type'] = $object_type; } elseif ( ! empty( $tax->object_type ) ) { $args['post_type'] = reset( $tax->object_type ); } if ( $tax->show_ui ) { $location = add_query_arg( $args, admin_url( 'term.php' ) ); } else { $location = ''; } /** * Filters the edit link for a term. * * @since 3.1.0 * * @param string $location The edit link. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy name. * @param string $object_type The object type. */ return apply_filters( 'get_edit_term_link', $location, $term_id, $taxonomy, $object_type ); } /** * Displays or retrieves the edit term link with formatting. * * @since 3.1.0 * * @param string $link Optional. Anchor text. If empty, default is 'Edit This'. Default empty. * @param string $before Optional. Display before edit link. Default empty. * @param string $after Optional. Display after edit link. Default empty. * @param int|WP_Term|null $term Optional. Term ID or object. If null, the queried object will be inspected. Default null. * @param bool $display Optional. Whether or not to echo the return. Default true. * @return string|void HTML content. */ function edit_term_link( $link = '', $before = '', $after = '', $term = null, $display = true ) { if ( is_null( $term ) ) { $term = get_queried_object(); } else { $term = get_term( $term ); } if ( ! $term ) { return; } $tax = get_taxonomy( $term->taxonomy ); if ( ! current_user_can( 'edit_term', $term->term_id ) ) { return; } if ( empty( $link ) ) { $link = __( 'Edit This' ); } $link = '' . $link . ''; /** * Filters the anchor tag for the edit link of a term. * * @since 3.1.0 * * @param string $link The anchor tag for the edit link. * @param int $term_id Term ID. */ $link = $before . apply_filters( 'edit_term_link', $link, $term->term_id ) . $after; if ( $display ) { echo $link; } else { return $link; } } /** * Retrieves the permalink for a search. * * @since 3.0.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param string $query Optional. The query string to use. If empty the current query is used. Default empty. * @return string The search permalink. */ function get_search_link( $query = '' ) { global $wp_rewrite; if ( empty( $query ) ) { $search = get_search_query( false ); } else { $search = stripslashes( $query ); } $permastruct = $wp_rewrite->get_search_permastruct(); if ( empty( $permastruct ) ) { $link = home_url( '?s=' . urlencode( $search ) ); } else { $search = urlencode( $search ); $search = str_replace( '%2F', '/', $search ); // %2F(/) is not valid within a URL, send it un-encoded. $link = str_replace( '%search%', $search, $permastruct ); $link = home_url( user_trailingslashit( $link, 'search' ) ); } /** * Filters the search permalink. * * @since 3.0.0 * * @param string $link Search permalink. * @param string $search The URL-encoded search term. */ return apply_filters( 'search_link', $link, $search ); } /** * Retrieves the permalink for the search results feed. * * @since 2.5.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param string $search_query Optional. Search query. Default empty. * @param string $feed Optional. Feed type. Possible values include 'rss2', 'atom'. * Default is the value of get_default_feed(). * @return string The search results feed permalink. */ function get_search_feed_link( $search_query = '', $feed = '' ) { global $wp_rewrite; $link = get_search_link( $search_query ); if ( empty( $feed ) ) { $feed = get_default_feed(); } $permastruct = $wp_rewrite->get_search_permastruct(); if ( empty( $permastruct ) ) { $link = add_query_arg( 'feed', $feed, $link ); } else { $link = trailingslashit( $link ); $link .= "feed/$feed/"; } /** * Filters the search feed link. * * @since 2.5.0 * * @param string $link Search feed link. * @param string $feed Feed type. Possible values include 'rss2', 'atom'. * @param string $type The search type. One of 'posts' or 'comments'. */ return apply_filters( 'search_feed_link', $link, $feed, 'posts' ); } /** * Retrieves the permalink for the search results comments feed. * * @since 2.5.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param string $search_query Optional. Search query. Default empty. * @param string $feed Optional. Feed type. Possible values include 'rss2', 'atom'. * Default is the value of get_default_feed(). * @return string The comments feed search results permalink. */ function get_search_comments_feed_link( $search_query = '', $feed = '' ) { global $wp_rewrite; if ( empty( $feed ) ) { $feed = get_default_feed(); } $link = get_search_feed_link( $search_query, $feed ); $permastruct = $wp_rewrite->get_search_permastruct(); if ( empty( $permastruct ) ) { $link = add_query_arg( 'feed', 'comments-' . $feed, $link ); } else { $link = add_query_arg( 'withcomments', 1, $link ); } /** This filter is documented in wp-includes/link-template.php */ return apply_filters( 'search_feed_link', $link, $feed, 'comments' ); } /** * Retrieves the permalink for a post type archive. * * @since 3.1.0 * @since 4.5.0 Support for posts was added. * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param string $post_type Post type. * @return string|false The post type archive permalink. False if the post type * does not exist or does not have an archive. */ function get_post_type_archive_link( $post_type ) { global $wp_rewrite; $post_type_obj = get_post_type_object( $post_type ); if ( ! $post_type_obj ) { return false; } if ( 'post' === $post_type ) { $show_on_front = get_option( 'show_on_front' ); $page_for_posts = get_option( 'page_for_posts' ); if ( 'page' === $show_on_front && $page_for_posts ) { $link = get_permalink( $page_for_posts ); } else { $link = get_home_url(); } /** This filter is documented in wp-includes/link-template.php */ return apply_filters( 'post_type_archive_link', $link, $post_type ); } if ( ! $post_type_obj->has_archive ) { return false; } if ( get_option( 'permalink_structure' ) && is_array( $post_type_obj->rewrite ) ) { $struct = ( true === $post_type_obj->has_archive ) ? $post_type_obj->rewrite['slug'] : $post_type_obj->has_archive; if ( $post_type_obj->rewrite['with_front'] ) { $struct = $wp_rewrite->front . $struct; } else { $struct = $wp_rewrite->root . $struct; } $link = home_url( user_trailingslashit( $struct, 'post_type_archive' ) ); } else { $link = home_url( '?post_type=' . $post_type ); } /** * Filters the post type archive permalink. * * @since 3.1.0 * * @param string $link The post type archive permalink. * @param string $post_type Post type name. */ return apply_filters( 'post_type_archive_link', $link, $post_type ); } /** * Retrieves the permalink for a post type archive feed. * * @since 3.1.0 * * @param string $post_type Post type. * @param string $feed Optional. Feed type. Possible values include 'rss2', 'atom'. * Default is the value of get_default_feed(). * @return string|false The post type feed permalink. False if the post type * does not exist or does not have an archive. */ function get_post_type_archive_feed_link( $post_type, $feed = '' ) { $default_feed = get_default_feed(); if ( empty( $feed ) ) { $feed = $default_feed; } $link = get_post_type_archive_link( $post_type ); if ( ! $link ) { return false; } $post_type_obj = get_post_type_object( $post_type ); if ( get_option( 'permalink_structure' ) && is_array( $post_type_obj->rewrite ) && $post_type_obj->rewrite['feeds'] ) { $link = trailingslashit( $link ); $link .= 'feed/'; if ( $feed != $default_feed ) { $link .= "$feed/"; } } else { $link = add_query_arg( 'feed', $feed, $link ); } /** * Filters the post type archive feed link. * * @since 3.1.0 * * @param string $link The post type archive feed link. * @param string $feed Feed type. Possible values include 'rss2', 'atom'. */ return apply_filters( 'post_type_archive_feed_link', $link, $feed ); } /** * Retrieves the URL used for the post preview. * * Allows additional query args to be appended. * * @since 4.4.0 * * @param int|WP_Post $post Optional. Post ID or `WP_Post` object. Defaults to global `$post`. * @param array $query_args Optional. Array of additional query args to be appended to the link. * Default empty array. * @param string $preview_link Optional. Base preview link to be used if it should differ from the * post permalink. Default empty. * @return string|null URL used for the post preview, or null if the post does not exist. */ function get_preview_post_link( $post = null, $query_args = array(), $preview_link = '' ) { $post = get_post( $post ); if ( ! $post ) { return; } $post_type_object = get_post_type_object( $post->post_type ); if ( is_post_type_viewable( $post_type_object ) ) { if ( ! $preview_link ) { $preview_link = set_url_scheme( get_permalink( $post ) ); } $query_args['preview'] = 'true'; $preview_link = add_query_arg( $query_args, $preview_link ); } /** * Filters the URL used for a post preview. * * @since 2.0.5 * @since 4.0.0 Added the `$post` parameter. * * @param string $preview_link URL used for the post preview. * @param WP_Post $post Post object. */ return apply_filters( 'preview_post_link', $preview_link, $post ); } /** * Retrieves the edit post link for post. * * Can be used within the WordPress loop or outside of it. Can be used with * pages, posts, attachments, revisions, global styles, templates, and template parts. * * @since 2.3.0 * @since 6.3.0 Adds custom link for wp_navigation post types. * Adds custom links for wp_template_part and wp_template post types. * * @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`. * @param string $context Optional. How to output the '&' character. Default '&'. * @return string|null The edit post link for the given post. Null if the post type does not exist * or does not allow an editing UI. */ function get_edit_post_link( $post = 0, $context = 'display' ) { $post = get_post( $post ); if ( ! $post ) { return; } if ( 'revision' === $post->post_type ) { $action = ''; } elseif ( 'display' === $context ) { $action = '&action=edit'; } else { $action = '&action=edit'; } $post_type_object = get_post_type_object( $post->post_type ); if ( ! $post_type_object ) { return; } if ( ! current_user_can( 'edit_post', $post->ID ) ) { return; } $link = ''; if ( 'wp_template' === $post->post_type || 'wp_template_part' === $post->post_type ) { $slug = urlencode( get_stylesheet() . '//' . $post->post_name ); $link = admin_url( sprintf( $post_type_object->_edit_link, $post->post_type, $slug ) ); } elseif ( 'wp_navigation' === $post->post_type ) { $link = admin_url( sprintf( $post_type_object->_edit_link, (string) $post->ID ) ); } elseif ( $post_type_object->_edit_link ) { $link = admin_url( sprintf( $post_type_object->_edit_link . $action, $post->ID ) ); } /** * Filters the post edit link. * * @since 2.3.0 * * @param string $link The edit link. * @param int $post_id Post ID. * @param string $context The link context. If set to 'display' then ampersands * are encoded. */ return apply_filters( 'get_edit_post_link', $link, $post->ID, $context ); } /** * Displays the edit post link for post. * * @since 1.0.0 * @since 4.4.0 The `$css_class` argument was added. * * @param string $text Optional. Anchor text. If null, default is 'Edit This'. Default null. * @param string $before Optional. Display before edit link. Default empty. * @param string $after Optional. Display after edit link. Default empty. * @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`. * @param string $css_class Optional. Add custom class to link. Default 'post-edit-link'. */ function edit_post_link( $text = null, $before = '', $after = '', $post = 0, $css_class = 'post-edit-link' ) { $post = get_post( $post ); if ( ! $post ) { return; } $url = get_edit_post_link( $post->ID ); if ( ! $url ) { return; } if ( null === $text ) { $text = __( 'Edit This' ); } $link = '' . $text . ''; /** * Filters the post edit link anchor tag. * * @since 2.3.0 * * @param string $link Anchor tag for the edit link. * @param int $post_id Post ID. * @param string $text Anchor text. */ echo $before . apply_filters( 'edit_post_link', $link, $post->ID, $text ) . $after; } /** * Retrieves the delete posts link for post. * * Can be used within the WordPress loop or outside of it, with any post type. * * @since 2.9.0 * * @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`. * @param string $deprecated Not used. * @param bool $force_delete Optional. Whether to bypass Trash and force deletion. Default false. * @return string|void The delete post link URL for the given post. */ function get_delete_post_link( $post = 0, $deprecated = '', $force_delete = false ) { if ( ! empty( $deprecated ) ) { _deprecated_argument( __FUNCTION__, '3.0.0' ); } $post = get_post( $post ); if ( ! $post ) { return; } $post_type_object = get_post_type_object( $post->post_type ); if ( ! $post_type_object ) { return; } if ( ! current_user_can( 'delete_post', $post->ID ) ) { return; } $action = ( $force_delete || ! EMPTY_TRASH_DAYS ) ? 'delete' : 'trash'; $delete_link = add_query_arg( 'action', $action, admin_url( sprintf( $post_type_object->_edit_link, $post->ID ) ) ); /** * Filters the post delete link. * * @since 2.9.0 * * @param string $link The delete link. * @param int $post_id Post ID. * @param bool $force_delete Whether to bypass the Trash and force deletion. Default false. */ return apply_filters( 'get_delete_post_link', wp_nonce_url( $delete_link, "$action-post_{$post->ID}" ), $post->ID, $force_delete ); } /** * Retrieves the edit comment link. * * @since 2.3.0 * * @param int|WP_Comment $comment_id Optional. Comment ID or WP_Comment object. * @return string|void The edit comment link URL for the given comment. */ function get_edit_comment_link( $comment_id = 0 ) { $comment = get_comment( $comment_id ); if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) { return; } $location = admin_url( 'comment.php?action=editcomment&c=' ) . $comment->comment_ID; /** * Filters the comment edit link. * * @since 2.3.0 * * @param string $location The edit link. */ return apply_filters( 'get_edit_comment_link', $location ); } /** * Displays the edit comment link with formatting. * * @since 1.0.0 * * @param string $text Optional. Anchor text. If null, default is 'Edit This'. Default null. * @param string $before Optional. Display before edit link. Default empty. * @param string $after Optional. Display after edit link. Default empty. */ function edit_comment_link( $text = null, $before = '', $after = '' ) { $comment = get_comment(); if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) { return; } if ( null === $text ) { $text = __( 'Edit This' ); } $link = '' . $text . ''; /** * Filters the comment edit link anchor tag. * * @since 2.3.0 * * @param string $link Anchor tag for the edit link. * @param string $comment_id Comment ID as a numeric string. * @param string $text Anchor text. */ echo $before . apply_filters( 'edit_comment_link', $link, $comment->comment_ID, $text ) . $after; } /** * Displays the edit bookmark link. * * @since 2.7.0 * * @param int|stdClass $link Optional. Bookmark ID. Default is the ID of the current bookmark. * @return string|void The edit bookmark link URL. */ function get_edit_bookmark_link( $link = 0 ) { $link = get_bookmark( $link ); if ( ! current_user_can( 'manage_links' ) ) { return; } $location = admin_url( 'link.php?action=edit&link_id=' ) . $link->link_id; /** * Filters the bookmark edit link. * * @since 2.7.0 * * @param string $location The edit link. * @param int $link_id Bookmark ID. */ return apply_filters( 'get_edit_bookmark_link', $location, $link->link_id ); } /** * Displays the edit bookmark link anchor content. * * @since 2.7.0 * * @param string $link Optional. Anchor text. If empty, default is 'Edit This'. Default empty. * @param string $before Optional. Display before edit link. Default empty. * @param string $after Optional. Display after edit link. Default empty. * @param int $bookmark Optional. Bookmark ID. Default is the current bookmark. */ function edit_bookmark_link( $link = '', $before = '', $after = '', $bookmark = null ) { $bookmark = get_bookmark( $bookmark ); if ( ! current_user_can( 'manage_links' ) ) { return; } if ( empty( $link ) ) { $link = __( 'Edit This' ); } $link = '' . $link . ''; /** * Filters the bookmark edit link anchor tag. * * @since 2.7.0 * * @param string $link Anchor tag for the edit link. * @param int $link_id Bookmark ID. */ echo $before . apply_filters( 'edit_bookmark_link', $link, $bookmark->link_id ) . $after; } /** * Retrieves the edit user link. * * @since 3.5.0 * * @param int $user_id Optional. User ID. Defaults to the current user. * @return string URL to edit user page or empty string. */ function get_edit_user_link( $user_id = null ) { if ( ! $user_id ) { $user_id = get_current_user_id(); } if ( empty( $user_id ) || ! current_user_can( 'edit_user', $user_id ) ) { return ''; } $user = get_userdata( $user_id ); if ( ! $user ) { return ''; } if ( get_current_user_id() == $user->ID ) { $link = get_edit_profile_url( $user->ID ); } else { $link = add_query_arg( 'user_id', $user->ID, self_admin_url( 'user-edit.php' ) ); } /** * Filters the user edit link. * * @since 3.5.0 * * @param string $link The edit link. * @param int $user_id User ID. */ return apply_filters( 'get_edit_user_link', $link, $user->ID ); } // // Navigation links. // /** * Retrieves the previous post that is adjacent to the current post. * * @since 1.5.0 * * @param bool $in_same_term Optional. Whether post should be in the same taxonomy term. * Default false. * @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded term IDs. * Default empty. * @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'. * @return WP_Post|null|string Post object if successful. Null if global `$post` is not set. * Empty string if no corresponding post exists. */ function get_previous_post( $in_same_term = false, $excluded_terms = '', $taxonomy = 'category' ) { return get_adjacent_post( $in_same_term, $excluded_terms, true, $taxonomy ); } /** * Retrieves the next post that is adjacent to the current post. * * @since 1.5.0 * * @param bool $in_same_term Optional. Whether post should be in the same taxonomy term. * Default false. * @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded term IDs. * Default empty. * @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'. * @return WP_Post|null|string Post object if successful. Null if global `$post` is not set. * Empty string if no corresponding post exists. */ function get_next_post( $in_same_term = false, $excluded_terms = '', $taxonomy = 'category' ) { return get_adjacent_post( $in_same_term, $excluded_terms, false, $taxonomy ); } /** * Retrieves the adjacent post. * * Can either be next or previous post. * * @since 2.5.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param bool $in_same_term Optional. Whether post should be in the same taxonomy term. * Default false. * @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded term IDs. * Default empty string. * @param bool $previous Optional. Whether to retrieve previous post. * Default true. * @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'. * @return WP_Post|null|string Post object if successful. Null if global `$post` is not set. * Empty string if no corresponding post exists. */ function get_adjacent_post( $in_same_term = false, $excluded_terms = '', $previous = true, $taxonomy = 'category' ) { global $wpdb; $post = get_post(); if ( ! $post || ! taxonomy_exists( $taxonomy ) ) { return null; } $current_post_date = $post->post_date; $join = ''; $where = ''; $adjacent = $previous ? 'previous' : 'next'; if ( ! empty( $excluded_terms ) && ! is_array( $excluded_terms ) ) { // Back-compat, $excluded_terms used to be $excluded_categories with IDs separated by " and ". if ( str_contains( $excluded_terms, ' and ' ) ) { _deprecated_argument( __FUNCTION__, '3.3.0', sprintf( /* translators: %s: The word 'and'. */ __( 'Use commas instead of %s to separate excluded terms.' ), "'and'" ) ); $excluded_terms = explode( ' and ', $excluded_terms ); } else { $excluded_terms = explode( ',', $excluded_terms ); } $excluded_terms = array_map( 'intval', $excluded_terms ); } /** * Filters the IDs of terms excluded from adjacent post queries. * * The dynamic portion of the hook name, `$adjacent`, refers to the type * of adjacency, 'next' or 'previous'. * * Possible hook names include: * * - `get_next_post_excluded_terms` * - `get_previous_post_excluded_terms` * * @since 4.4.0 * * @param int[]|string $excluded_terms Array of excluded term IDs. Empty string if none were provided. */ $excluded_terms = apply_filters( "get_{$adjacent}_post_excluded_terms", $excluded_terms ); if ( $in_same_term || ! empty( $excluded_terms ) ) { if ( $in_same_term ) { $join .= " INNER JOIN $wpdb->term_relationships AS tr ON p.ID = tr.object_id INNER JOIN $wpdb->term_taxonomy AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id"; $where .= $wpdb->prepare( 'AND tt.taxonomy = %s', $taxonomy ); if ( ! is_object_in_taxonomy( $post->post_type, $taxonomy ) ) { return ''; } $term_array = wp_get_object_terms( $post->ID, $taxonomy, array( 'fields' => 'ids' ) ); // Remove any exclusions from the term array to include. $term_array = array_diff( $term_array, (array) $excluded_terms ); $term_array = array_map( 'intval', $term_array ); if ( ! $term_array || is_wp_error( $term_array ) ) { return ''; } $where .= ' AND tt.term_id IN (' . implode( ',', $term_array ) . ')'; } if ( ! empty( $excluded_terms ) ) { $where .= " AND p.ID NOT IN ( SELECT tr.object_id FROM $wpdb->term_relationships tr LEFT JOIN $wpdb->term_taxonomy tt ON (tr.term_taxonomy_id = tt.term_taxonomy_id) WHERE tt.term_id IN (" . implode( ',', array_map( 'intval', $excluded_terms ) ) . ') )'; } } // 'post_status' clause depends on the current user. if ( is_user_logged_in() ) { $user_id = get_current_user_id(); $post_type_object = get_post_type_object( $post->post_type ); if ( empty( $post_type_object ) ) { $post_type_cap = $post->post_type; $read_private_cap = 'read_private_' . $post_type_cap . 's'; } else { $read_private_cap = $post_type_object->cap->read_private_posts; } /* * Results should include private posts belonging to the current user, or private posts where the * current user has the 'read_private_posts' cap. */ $private_states = get_post_stati( array( 'private' => true ) ); $where .= " AND ( p.post_status = 'publish'"; foreach ( $private_states as $state ) { if ( current_user_can( $read_private_cap ) ) { $where .= $wpdb->prepare( ' OR p.post_status = %s', $state ); } else { $where .= $wpdb->prepare( ' OR (p.post_author = %d AND p.post_status = %s)', $user_id, $state ); } } $where .= ' )'; } else { $where .= " AND p.post_status = 'publish'"; } $op = $previous ? '<' : '>'; $order = $previous ? 'DESC' : 'ASC'; /** * Filters the JOIN clause in the SQL for an adjacent post query. * * The dynamic portion of the hook name, `$adjacent`, refers to the type * of adjacency, 'next' or 'previous'. * * Possible hook names include: * * - `get_next_post_join` * - `get_previous_post_join` * * @since 2.5.0 * @since 4.4.0 Added the `$taxonomy` and `$post` parameters. * * @param string $join The JOIN clause in the SQL. * @param bool $in_same_term Whether post should be in the same taxonomy term. * @param int[]|string $excluded_terms Array of excluded term IDs. Empty string if none were provided. * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true. * @param WP_Post $post WP_Post object. */ $join = apply_filters( "get_{$adjacent}_post_join", $join, $in_same_term, $excluded_terms, $taxonomy, $post ); /** * Filters the WHERE clause in the SQL for an adjacent post query. * * The dynamic portion of the hook name, `$adjacent`, refers to the type * of adjacency, 'next' or 'previous'. * * Possible hook names include: * * - `get_next_post_where` * - `get_previous_post_where` * * @since 2.5.0 * @since 4.4.0 Added the `$taxonomy` and `$post` parameters. * * @param string $where The `WHERE` clause in the SQL. * @param bool $in_same_term Whether post should be in the same taxonomy term. * @param int[]|string $excluded_terms Array of excluded term IDs. Empty string if none were provided. * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true. * @param WP_Post $post WP_Post object. */ $where = apply_filters( "get_{$adjacent}_post_where", $wpdb->prepare( "WHERE p.post_date $op %s AND p.post_type = %s $where", $current_post_date, $post->post_type ), $in_same_term, $excluded_terms, $taxonomy, $post ); /** * Filters the ORDER BY clause in the SQL for an adjacent post query. * * The dynamic portion of the hook name, `$adjacent`, refers to the type * of adjacency, 'next' or 'previous'. * * Possible hook names include: * * - `get_next_post_sort` * - `get_previous_post_sort` * * @since 2.5.0 * @since 4.4.0 Added the `$post` parameter. * @since 4.9.0 Added the `$order` parameter. * * @param string $order_by The `ORDER BY` clause in the SQL. * @param WP_Post $post WP_Post object. * @param string $order Sort order. 'DESC' for previous post, 'ASC' for next. */ $sort = apply_filters( "get_{$adjacent}_post_sort", "ORDER BY p.post_date $order LIMIT 1", $post, $order ); $query = "SELECT p.ID FROM $wpdb->posts AS p $join $where $sort"; $key = md5( $query ); $last_changed = wp_cache_get_last_changed( 'posts' ); if ( $in_same_term || ! empty( $excluded_terms ) ) { $last_changed .= wp_cache_get_last_changed( 'terms' ); } $cache_key = "adjacent_post:$key:$last_changed"; $result = wp_cache_get( $cache_key, 'post-queries' ); if ( false !== $result ) { if ( $result ) { $result = get_post( $result ); } return $result; } $result = $wpdb->get_var( $query ); if ( null === $result ) { $result = ''; } wp_cache_set( $cache_key, $result, 'post-queries' ); if ( $result ) { $result = get_post( $result ); } return $result; } /** * Retrieves the adjacent post relational link. * * Can either be next or previous post relational link. * * @since 2.8.0 * * @param string $title Optional. Link title format. Default '%title'. * @param bool $in_same_term Optional. Whether link should be in the same taxonomy term. * Default false. * @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded term IDs. * Default empty. * @param bool $previous Optional. Whether to display link to previous or next post. * Default true. * @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'. * @return string|void The adjacent post relational link URL. */ function get_adjacent_post_rel_link( $title = '%title', $in_same_term = false, $excluded_terms = '', $previous = true, $taxonomy = 'category' ) { $post = get_post(); if ( $previous && is_attachment() && $post ) { $post = get_post( $post->post_parent ); } else { $post = get_adjacent_post( $in_same_term, $excluded_terms, $previous, $taxonomy ); } if ( empty( $post ) ) { return; } $post_title = the_title_attribute( array( 'echo' => false, 'post' => $post, ) ); if ( empty( $post_title ) ) { $post_title = $previous ? __( 'Previous Post' ) : __( 'Next Post' ); } $date = mysql2date( get_option( 'date_format' ), $post->post_date ); $title = str_replace( '%title', $post_title, $title ); $title = str_replace( '%date', $date, $title ); $link = $previous ? "\n"; $adjacent = $previous ? 'previous' : 'next'; /** * Filters the adjacent post relational link. * * The dynamic portion of the hook name, `$adjacent`, refers to the type * of adjacency, 'next' or 'previous'. * * Possible hook names include: * * - `next_post_rel_link` * - `previous_post_rel_link` * * @since 2.8.0 * * @param string $link The relational link. */ return apply_filters( "{$adjacent}_post_rel_link", $link ); } /** * Displays the relational links for the posts adjacent to the current post. * * @since 2.8.0 * * @param string $title Optional. Link title format. Default '%title'. * @param bool $in_same_term Optional. Whether link should be in the same taxonomy term. * Default false. * @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded term IDs. * Default empty. * @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'. */ function adjacent_posts_rel_link( $title = '%title', $in_same_term = false, $excluded_terms = '', $taxonomy = 'category' ) { echo get_adjacent_post_rel_link( $title, $in_same_term, $excluded_terms, true, $taxonomy ); echo get_adjacent_post_rel_link( $title, $in_same_term, $excluded_terms, false, $taxonomy ); } /** * Displays relational links for the posts adjacent to the current post for single post pages. * * This is meant to be attached to actions like 'wp_head'. Do not call this directly in plugins * or theme templates. * * @since 3.0.0 * @since 5.6.0 No longer used in core. * * @see adjacent_posts_rel_link() */ function adjacent_posts_rel_link_wp_head() { if ( ! is_single() || is_attachment() ) { return; } adjacent_posts_rel_link(); } /** * Displays the relational link for the next post adjacent to the current post. * * @since 2.8.0 * * @see get_adjacent_post_rel_link() * * @param string $title Optional. Link title format. Default '%title'. * @param bool $in_same_term Optional. Whether link should be in the same taxonomy term. * Default false. * @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded term IDs. * Default empty. * @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'. */ function next_post_rel_link( $title = '%title', $in_same_term = false, $excluded_terms = '', $taxonomy = 'category' ) { echo get_adjacent_post_rel_link( $title, $in_same_term, $excluded_terms, false, $taxonomy ); } /** * Displays the relational link for the previous post adjacent to the current post. * * @since 2.8.0 * * @see get_adjacent_post_rel_link() * * @param string $title Optional. Link title format. Default '%title'. * @param bool $in_same_term Optional. Whether link should be in the same taxonomy term. * Default false. * @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded term IDs. * Default true. * @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'. */ function prev_post_rel_link( $title = '%title', $in_same_term = false, $excluded_terms = '', $taxonomy = 'category' ) { echo get_adjacent_post_rel_link( $title, $in_same_term, $excluded_terms, true, $taxonomy ); } /** * Retrieves the boundary post. * * Boundary being either the first or last post by publish date within the constraints specified * by `$in_same_term` or `$excluded_terms`. * * @since 2.8.0 * * @param bool $in_same_term Optional. Whether returned post should be in the same taxonomy term. * Default false. * @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded term IDs. * Default empty. * @param bool $start Optional. Whether to retrieve first or last post. * Default true. * @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'. * @return array|null Array containing the boundary post object if successful, null otherwise. */ function get_boundary_post( $in_same_term = false, $excluded_terms = '', $start = true, $taxonomy = 'category' ) { $post = get_post(); if ( ! $post || ! is_single() || is_attachment() || ! taxonomy_exists( $taxonomy ) ) { return null; } $query_args = array( 'posts_per_page' => 1, 'order' => $start ? 'ASC' : 'DESC', 'update_post_term_cache' => false, 'update_post_meta_cache' => false, ); $term_array = array(); if ( ! is_array( $excluded_terms ) ) { if ( ! empty( $excluded_terms ) ) { $excluded_terms = explode( ',', $excluded_terms ); } else { $excluded_terms = array(); } } if ( $in_same_term || ! empty( $excluded_terms ) ) { if ( $in_same_term ) { $term_array = wp_get_object_terms( $post->ID, $taxonomy, array( 'fields' => 'ids' ) ); } if ( ! empty( $excluded_terms ) ) { $excluded_terms = array_map( 'intval', $excluded_terms ); $excluded_terms = array_diff( $excluded_terms, $term_array ); $inverse_terms = array(); foreach ( $excluded_terms as $excluded_term ) { $inverse_terms[] = $excluded_term * -1; } $excluded_terms = $inverse_terms; } $query_args['tax_query'] = array( array( 'taxonomy' => $taxonomy, 'terms' => array_merge( $term_array, $excluded_terms ), ), ); } return get_posts( $query_args ); } /** * Retrieves the previous post link that is adjacent to the current post. * * @since 3.7.0 * * @param string $format Optional. Link anchor format. Default '« %link'. * @param string $link Optional. Link permalink format. Default '%title'. * @param bool $in_same_term Optional. Whether link should be in the same taxonomy term. * Default false. * @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded term IDs. * Default empty. * @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'. * @return string The link URL of the previous post in relation to the current post. */ function get_previous_post_link( $format = '« %link', $link = '%title', $in_same_term = false, $excluded_terms = '', $taxonomy = 'category' ) { return get_adjacent_post_link( $format, $link, $in_same_term, $excluded_terms, true, $taxonomy ); } /** * Displays the previous post link that is adjacent to the current post. * * @since 1.5.0 * * @see get_previous_post_link() * * @param string $format Optional. Link anchor format. Default '« %link'. * @param string $link Optional. Link permalink format. Default '%title'. * @param bool $in_same_term Optional. Whether link should be in the same taxonomy term. * Default false. * @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded term IDs. * Default empty. * @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'. */ function previous_post_link( $format = '« %link', $link = '%title', $in_same_term = false, $excluded_terms = '', $taxonomy = 'category' ) { echo get_previous_post_link( $format, $link, $in_same_term, $excluded_terms, $taxonomy ); } /** * Retrieves the next post link that is adjacent to the current post. * * @since 3.7.0 * * @param string $format Optional. Link anchor format. Default '« %link'. * @param string $link Optional. Link permalink format. Default '%title'. * @param bool $in_same_term Optional. Whether link should be in the same taxonomy term. * Default false. * @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded term IDs. * Default empty. * @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'. * @return string The link URL of the next post in relation to the current post. */ function get_next_post_link( $format = '%link »', $link = '%title', $in_same_term = false, $excluded_terms = '', $taxonomy = 'category' ) { return get_adjacent_post_link( $format, $link, $in_same_term, $excluded_terms, false, $taxonomy ); } /** * Displays the next post link that is adjacent to the current post. * * @since 1.5.0 * * @see get_next_post_link() * * @param string $format Optional. Link anchor format. Default '« %link'. * @param string $link Optional. Link permalink format. Default '%title'. * @param bool $in_same_term Optional. Whether link should be in the same taxonomy term. * Default false. * @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded term IDs. * Default empty. * @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'. */ function next_post_link( $format = '%link »', $link = '%title', $in_same_term = false, $excluded_terms = '', $taxonomy = 'category' ) { echo get_next_post_link( $format, $link, $in_same_term, $excluded_terms, $taxonomy ); } /** * Retrieves the adjacent post link. * * Can be either next post link or previous. * * @since 3.7.0 * * @param string $format Link anchor format. * @param string $link Link permalink format. * @param bool $in_same_term Optional. Whether link should be in the same taxonomy term. * Default false. * @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded terms IDs. * Default empty. * @param bool $previous Optional. Whether to display link to previous or next post. * Default true. * @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'. * @return string The link URL of the previous or next post in relation to the current post. */ function get_adjacent_post_link( $format, $link, $in_same_term = false, $excluded_terms = '', $previous = true, $taxonomy = 'category' ) { if ( $previous && is_attachment() ) { $post = get_post( get_post()->post_parent ); } else { $post = get_adjacent_post( $in_same_term, $excluded_terms, $previous, $taxonomy ); } if ( ! $post ) { $output = ''; } else { $title = $post->post_title; if ( empty( $post->post_title ) ) { $title = $previous ? __( 'Previous Post' ) : __( 'Next Post' ); } /** This filter is documented in wp-includes/post-template.php */ $title = apply_filters( 'the_title', $title, $post->ID ); $date = mysql2date( get_option( 'date_format' ), $post->post_date ); $rel = $previous ? 'prev' : 'next'; $string = ''; $inlink = str_replace( '%title', $title, $link ); $inlink = str_replace( '%date', $date, $inlink ); $inlink = $string . $inlink . ''; $output = str_replace( '%link', $inlink, $format ); } $adjacent = $previous ? 'previous' : 'next'; /** * Filters the adjacent post link. * * The dynamic portion of the hook name, `$adjacent`, refers to the type * of adjacency, 'next' or 'previous'. * * Possible hook names include: * * - `next_post_link` * - `previous_post_link` * * @since 2.6.0 * @since 4.2.0 Added the `$adjacent` parameter. * * @param string $output The adjacent post link. * @param string $format Link anchor format. * @param string $link Link permalink format. * @param WP_Post|string $post The adjacent post. Empty string if no corresponding post exists. * @param string $adjacent Whether the post is previous or next. */ return apply_filters( "{$adjacent}_post_link", $output, $format, $link, $post, $adjacent ); } /** * Displays the adjacent post link. * * Can be either next post link or previous. * * @since 2.5.0 * * @param string $format Link anchor format. * @param string $link Link permalink format. * @param bool $in_same_term Optional. Whether link should be in the same taxonomy term. * Default false. * @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded category IDs. * Default empty. * @param bool $previous Optional. Whether to display link to previous or next post. * Default true. * @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'. */ function adjacent_post_link( $format, $link, $in_same_term = false, $excluded_terms = '', $previous = true, $taxonomy = 'category' ) { echo get_adjacent_post_link( $format, $link, $in_same_term, $excluded_terms, $previous, $taxonomy ); } /** * Retrieves the link for a page number. * * @since 1.5.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param int $pagenum Optional. Page number. Default 1. * @param bool $escape Optional. Whether to escape the URL for display, with esc_url(). * If set to false, prepares the URL with sanitize_url(). Default true. * @return string The link URL for the given page number. */ function get_pagenum_link( $pagenum = 1, $escape = true ) { global $wp_rewrite; $pagenum = (int) $pagenum; $request = remove_query_arg( 'paged' ); $home_root = parse_url( home_url() ); $home_root = ( isset( $home_root['path'] ) ) ? $home_root['path'] : ''; $home_root = preg_quote( $home_root, '|' ); $request = preg_replace( '|^' . $home_root . '|i', '', $request ); $request = preg_replace( '|^/+|', '', $request ); if ( ! $wp_rewrite->using_permalinks() || is_admin() ) { $base = trailingslashit( get_bloginfo( 'url' ) ); if ( $pagenum > 1 ) { $result = add_query_arg( 'paged', $pagenum, $base . $request ); } else { $result = $base . $request; } } else { $qs_regex = '|\?.*?$|'; preg_match( $qs_regex, $request, $qs_match ); $parts = array(); $parts[] = untrailingslashit( get_bloginfo( 'url' ) ); if ( ! empty( $qs_match[0] ) ) { $query_string = $qs_match[0]; $request = preg_replace( $qs_regex, '', $request ); } else { $query_string = ''; } $request = preg_replace( "|$wp_rewrite->pagination_base/\d+/?$|", '', $request ); $request = preg_replace( '|^' . preg_quote( $wp_rewrite->index, '|' ) . '|i', '', $request ); $request = ltrim( $request, '/' ); if ( $wp_rewrite->using_index_permalinks() && ( $pagenum > 1 || '' !== $request ) ) { $parts[] = $wp_rewrite->index; } $parts[] = untrailingslashit( $request ); if ( $pagenum > 1 ) { $parts[] = $wp_rewrite->pagination_base; $parts[] = $pagenum; } $result = user_trailingslashit( implode( '/', array_filter( $parts ) ), 'paged' ); if ( ! empty( $query_string ) ) { $result .= $query_string; } } /** * Filters the page number link for the current request. * * @since 2.5.0 * @since 5.2.0 Added the `$pagenum` argument. * * @param string $result The page number link. * @param int $pagenum The page number. */ $result = apply_filters( 'get_pagenum_link', $result, $pagenum ); if ( $escape ) { return esc_url( $result ); } else { return sanitize_url( $result ); } } /** * Retrieves the next posts page link. * * Backported from 2.1.3 to 2.0.10. * * @since 2.0.10 * * @global int $paged * * @param int $max_page Optional. Max pages. Default 0. * @return string|void The link URL for next posts page. */ function get_next_posts_page_link( $max_page = 0 ) { global $paged; if ( ! is_single() ) { if ( ! $paged ) { $paged = 1; } $next_page = (int) $paged + 1; if ( ! $max_page || $max_page >= $next_page ) { return get_pagenum_link( $next_page ); } } } /** * Displays or retrieves the next posts page link. * * @since 0.71 * * @param int $max_page Optional. Max pages. Default 0. * @param bool $display Optional. Whether to echo the link. Default true. * @return string|void The link URL for next posts page if `$display = false`. */ function next_posts( $max_page = 0, $display = true ) { $link = get_next_posts_page_link( $max_page ); $output = $link ? esc_url( $link ) : ''; if ( $display ) { echo $output; } else { return $output; } } /** * Retrieves the next posts page link. * * @since 2.7.0 * * @global int $paged * @global WP_Query $wp_query WordPress Query object. * * @param string $label Content for link text. * @param int $max_page Optional. Max pages. Default 0. * @return string|void HTML-formatted next posts page link. */ function get_next_posts_link( $label = null, $max_page = 0 ) { global $paged, $wp_query; if ( ! $max_page ) { $max_page = $wp_query->max_num_pages; } if ( ! $paged ) { $paged = 1; } $next_page = (int) $paged + 1; if ( null === $label ) { $label = __( 'Next Page »' ); } if ( ! is_single() && ( $next_page <= $max_page ) ) { /** * Filters the anchor tag attributes for the next posts page link. * * @since 2.7.0 * * @param string $attributes Attributes for the anchor tag. */ $attr = apply_filters( 'next_posts_link_attributes', '' ); return sprintf( '%3$s', next_posts( $max_page, false ), $attr, preg_replace( '/&([^#])(?![a-z]{1,8};)/i', '&$1', $label ) ); } } /** * Displays the next posts page link. * * @since 0.71 * * @param string $label Content for link text. * @param int $max_page Optional. Max pages. Default 0. */ function next_posts_link( $label = null, $max_page = 0 ) { echo get_next_posts_link( $label, $max_page ); } /** * Retrieves the previous posts page link. * * Will only return string, if not on a single page or post. * * Backported to 2.0.10 from 2.1.3. * * @since 2.0.10 * * @global int $paged * * @return string|void The link for the previous posts page. */ function get_previous_posts_page_link() { global $paged; if ( ! is_single() ) { $previous_page = (int) $paged - 1; if ( $previous_page < 1 ) { $previous_page = 1; } return get_pagenum_link( $previous_page ); } } /** * Displays or retrieves the previous posts page link. * * @since 0.71 * * @param bool $display Optional. Whether to echo the link. Default true. * @return string|void The previous posts page link if `$display = false`. */ function previous_posts( $display = true ) { $output = esc_url( get_previous_posts_page_link() ); if ( $display ) { echo $output; } else { return $output; } } /** * Retrieves the previous posts page link. * * @since 2.7.0 * * @global int $paged * * @param string $label Optional. Previous page link text. * @return string|void HTML-formatted previous page link. */ function get_previous_posts_link( $label = null ) { global $paged; if ( null === $label ) { $label = __( '« Previous Page' ); } if ( ! is_single() && $paged > 1 ) { /** * Filters the anchor tag attributes for the previous posts page link. * * @since 2.7.0 * * @param string $attributes Attributes for the anchor tag. */ $attr = apply_filters( 'previous_posts_link_attributes', '' ); return sprintf( '%3$s', previous_posts( false ), $attr, preg_replace( '/&([^#])(?![a-z]{1,8};)/i', '&$1', $label ) ); } } /** * Displays the previous posts page link. * * @since 0.71 * * @param string $label Optional. Previous page link text. */ function previous_posts_link( $label = null ) { echo get_previous_posts_link( $label ); } /** * Retrieves the post pages link navigation for previous and next pages. * * @since 2.8.0 * * @global WP_Query $wp_query WordPress Query object. * * @param string|array $args { * Optional. Arguments to build the post pages link navigation. * * @type string $sep Separator character. Default '—'. * @type string $prelabel Link text to display for the previous page link. * Default '« Previous Page'. * @type string $nxtlabel Link text to display for the next page link. * Default 'Next Page »'. * } * @return string The posts link navigation. */ function get_posts_nav_link( $args = array() ) { global $wp_query; $return = ''; if ( ! is_singular() ) { $defaults = array( 'sep' => ' — ', 'prelabel' => __( '« Previous Page' ), 'nxtlabel' => __( 'Next Page »' ), ); $args = wp_parse_args( $args, $defaults ); $max_num_pages = $wp_query->max_num_pages; $paged = get_query_var( 'paged' ); // Only have sep if there's both prev and next results. if ( $paged < 2 || $paged >= $max_num_pages ) { $args['sep'] = ''; } if ( $max_num_pages > 1 ) { $return = get_previous_posts_link( $args['prelabel'] ); $return .= preg_replace( '/&([^#])(?![a-z]{1,8};)/i', '&$1', $args['sep'] ); $return .= get_next_posts_link( $args['nxtlabel'] ); } } return $return; } /** * Displays the post pages link navigation for previous and next pages. * * @since 0.71 * * @param string $sep Optional. Separator for posts navigation links. Default empty. * @param string $prelabel Optional. Label for previous pages. Default empty. * @param string $nxtlabel Optional Label for next pages. Default empty. */ function posts_nav_link( $sep = '', $prelabel = '', $nxtlabel = '' ) { $args = array_filter( compact( 'sep', 'prelabel', 'nxtlabel' ) ); echo get_posts_nav_link( $args ); } /** * Retrieves the navigation to next/previous post, when applicable. * * @since 4.1.0 * @since 4.4.0 Introduced the `in_same_term`, `excluded_terms`, and `taxonomy` arguments. * @since 5.3.0 Added the `aria_label` parameter. * @since 5.5.0 Added the `class` parameter. * * @param array $args { * Optional. Default post navigation arguments. Default empty array. * * @type string $prev_text Anchor text to display in the previous post link. * Default '%title'. * @type string $next_text Anchor text to display in the next post link. * Default '%title'. * @type bool $in_same_term Whether link should be in the same taxonomy term. * Default false. * @type int[]|string $excluded_terms Array or comma-separated list of excluded term IDs. * Default empty. * @type string $taxonomy Taxonomy, if `$in_same_term` is true. Default 'category'. * @type string $screen_reader_text Screen reader text for the nav element. * Default 'Post navigation'. * @type string $aria_label ARIA label text for the nav element. Default 'Posts'. * @type string $class Custom class for the nav element. Default 'post-navigation'. * } * @return string Markup for post links. */ function get_the_post_navigation( $args = array() ) { // Make sure the nav element has an aria-label attribute: fallback to the screen reader text. if ( ! empty( $args['screen_reader_text'] ) && empty( $args['aria_label'] ) ) { $args['aria_label'] = $args['screen_reader_text']; } $args = wp_parse_args( $args, array( 'prev_text' => '%title', 'next_text' => '%title', 'in_same_term' => false, 'excluded_terms' => '', 'taxonomy' => 'category', 'screen_reader_text' => __( 'Post navigation' ), 'aria_label' => __( 'Posts' ), 'class' => 'post-navigation', ) ); $navigation = ''; $previous = get_previous_post_link( '', $args['prev_text'], $args['in_same_term'], $args['excluded_terms'], $args['taxonomy'] ); $next = get_next_post_link( '', $args['next_text'], $args['in_same_term'], $args['excluded_terms'], $args['taxonomy'] ); // Only add markup if there's somewhere to navigate to. if ( $previous || $next ) { $navigation = _navigation_markup( $previous . $next, $args['class'], $args['screen_reader_text'], $args['aria_label'] ); } return $navigation; } /** * Displays the navigation to next/previous post, when applicable. * * @since 4.1.0 * * @param array $args Optional. See get_the_post_navigation() for available arguments. * Default empty array. */ function the_post_navigation( $args = array() ) { echo get_the_post_navigation( $args ); } /** * Returns the navigation to next/previous set of posts, when applicable. * * @since 4.1.0 * @since 5.3.0 Added the `aria_label` parameter. * @since 5.5.0 Added the `class` parameter. * * @global WP_Query $wp_query WordPress Query object. * * @param array $args { * Optional. Default posts navigation arguments. Default empty array. * * @type string $prev_text Anchor text to display in the previous posts link. * Default 'Older posts'. * @type string $next_text Anchor text to display in the next posts link. * Default 'Newer posts'. * @type string $screen_reader_text Screen reader text for the nav element. * Default 'Posts navigation'. * @type string $aria_label ARIA label text for the nav element. Default 'Posts'. * @type string $class Custom class for the nav element. Default 'posts-navigation'. * } * @return string Markup for posts links. */ function get_the_posts_navigation( $args = array() ) { global $wp_query; $navigation = ''; // Don't print empty markup if there's only one page. if ( $wp_query->max_num_pages > 1 ) { // Make sure the nav element has an aria-label attribute: fallback to the screen reader text. if ( ! empty( $args['screen_reader_text'] ) && empty( $args['aria_label'] ) ) { $args['aria_label'] = $args['screen_reader_text']; } $args = wp_parse_args( $args, array( 'prev_text' => __( 'Older posts' ), 'next_text' => __( 'Newer posts' ), 'screen_reader_text' => __( 'Posts navigation' ), 'aria_label' => __( 'Posts' ), 'class' => 'posts-navigation', ) ); $next_link = get_previous_posts_link( $args['next_text'] ); $prev_link = get_next_posts_link( $args['prev_text'] ); if ( $prev_link ) { $navigation .= ''; } if ( $next_link ) { $navigation .= ''; } $navigation = _navigation_markup( $navigation, $args['class'], $args['screen_reader_text'], $args['aria_label'] ); } return $navigation; } /** * Displays the navigation to next/previous set of posts, when applicable. * * @since 4.1.0 * * @param array $args Optional. See get_the_posts_navigation() for available arguments. * Default empty array. */ function the_posts_navigation( $args = array() ) { echo get_the_posts_navigation( $args ); } /** * Retrieves a paginated navigation to next/previous set of posts, when applicable. * * @since 4.1.0 * @since 5.3.0 Added the `aria_label` parameter. * @since 5.5.0 Added the `class` parameter. * * @global WP_Query $wp_query WordPress Query object. * * @param array $args { * Optional. Default pagination arguments, see paginate_links(). * * @type string $screen_reader_text Screen reader text for navigation element. * Default 'Posts navigation'. * @type string $aria_label ARIA label text for the nav element. Default 'Posts'. * @type string $class Custom class for the nav element. Default 'pagination'. * } * @return string Markup for pagination links. */ function get_the_posts_pagination( $args = array() ) { global $wp_query; $navigation = ''; // Don't print empty markup if there's only one page. if ( $wp_query->max_num_pages > 1 ) { // Make sure the nav element has an aria-label attribute: fallback to the screen reader text. if ( ! empty( $args['screen_reader_text'] ) && empty( $args['aria_label'] ) ) { $args['aria_label'] = $args['screen_reader_text']; } $args = wp_parse_args( $args, array( 'mid_size' => 1, 'prev_text' => _x( 'Previous', 'previous set of posts' ), 'next_text' => _x( 'Next', 'next set of posts' ), 'screen_reader_text' => __( 'Posts navigation' ), 'aria_label' => __( 'Posts' ), 'class' => 'pagination', ) ); /** * Filters the arguments for posts pagination links. * * @since 6.1.0 * * @param array $args { * Optional. Default pagination arguments, see paginate_links(). * * @type string $screen_reader_text Screen reader text for navigation element. * Default 'Posts navigation'. * @type string $aria_label ARIA label text for the nav element. Default 'Posts'. * @type string $class Custom class for the nav element. Default 'pagination'. * } */ $args = apply_filters( 'the_posts_pagination_args', $args ); // Make sure we get a string back. Plain is the next best thing. if ( isset( $args['type'] ) && 'array' === $args['type'] ) { $args['type'] = 'plain'; } // Set up paginated links. $links = paginate_links( $args ); if ( $links ) { $navigation = _navigation_markup( $links, $args['class'], $args['screen_reader_text'], $args['aria_label'] ); } } return $navigation; } /** * Displays a paginated navigation to next/previous set of posts, when applicable. * * @since 4.1.0 * * @param array $args Optional. See get_the_posts_pagination() for available arguments. * Default empty array. */ function the_posts_pagination( $args = array() ) { echo get_the_posts_pagination( $args ); } /** * Wraps passed links in navigational markup. * * @since 4.1.0 * @since 5.3.0 Added the `aria_label` parameter. * @access private * * @param string $links Navigational links. * @param string $css_class Optional. Custom class for the nav element. * Default 'posts-navigation'. * @param string $screen_reader_text Optional. Screen reader text for the nav element. * Default 'Posts navigation'. * @param string $aria_label Optional. ARIA label for the nav element. * Defaults to the value of `$screen_reader_text`. * @return string Navigation template tag. */ function _navigation_markup( $links, $css_class = 'posts-navigation', $screen_reader_text = '', $aria_label = '' ) { if ( empty( $screen_reader_text ) ) { $screen_reader_text = /* translators: Hidden accessibility text. */ __( 'Posts navigation' ); } if ( empty( $aria_label ) ) { $aria_label = $screen_reader_text; } $template = ' '; /** * Filters the navigation markup template. * * Note: The filtered template HTML must contain specifiers for the navigation * class (%1$s), the screen-reader-text value (%2$s), placement of the navigation * links (%3$s), and ARIA label text if screen-reader-text does not fit that (%4$s): * * * * @since 4.4.0 * * @param string $template The default template. * @param string $css_class The class passed by the calling function. * @return string Navigation template. */ $template = apply_filters( 'navigation_markup_template', $template, $css_class ); return sprintf( $template, sanitize_html_class( $css_class ), esc_html( $screen_reader_text ), $links, esc_attr( $aria_label ) ); } /** * Retrieves the comments page number link. * * @since 2.7.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param int $pagenum Optional. Page number. Default 1. * @param int $max_page Optional. The maximum number of comment pages. Default 0. * @return string The comments page number link URL. */ function get_comments_pagenum_link( $pagenum = 1, $max_page = 0 ) { global $wp_rewrite; $pagenum = (int) $pagenum; $result = get_permalink(); if ( 'newest' === get_option( 'default_comments_page' ) ) { if ( $pagenum != $max_page ) { if ( $wp_rewrite->using_permalinks() ) { $result = user_trailingslashit( trailingslashit( $result ) . $wp_rewrite->comments_pagination_base . '-' . $pagenum, 'commentpaged' ); } else { $result = add_query_arg( 'cpage', $pagenum, $result ); } } } elseif ( $pagenum > 1 ) { if ( $wp_rewrite->using_permalinks() ) { $result = user_trailingslashit( trailingslashit( $result ) . $wp_rewrite->comments_pagination_base . '-' . $pagenum, 'commentpaged' ); } else { $result = add_query_arg( 'cpage', $pagenum, $result ); } } $result .= '#comments'; /** * Filters the comments page number link for the current request. * * @since 2.7.0 * * @param string $result The comments page number link. */ return apply_filters( 'get_comments_pagenum_link', $result ); } /** * Retrieves the link to the next comments page. * * @since 2.7.1 * * @global WP_Query $wp_query WordPress Query object. * * @param string $label Optional. Label for link text. Default empty. * @param int $max_page Optional. Max page. Default 0. * @return string|void HTML-formatted link for the next page of comments. */ function get_next_comments_link( $label = '', $max_page = 0 ) { global $wp_query; if ( ! is_singular() ) { return; } $page = get_query_var( 'cpage' ); if ( ! $page ) { $page = 1; } $next_page = (int) $page + 1; if ( empty( $max_page ) ) { $max_page = $wp_query->max_num_comment_pages; } if ( empty( $max_page ) ) { $max_page = get_comment_pages_count(); } if ( $next_page > $max_page ) { return; } if ( empty( $label ) ) { $label = __( 'Newer Comments »' ); } /** * Filters the anchor tag attributes for the next comments page link. * * @since 2.7.0 * * @param string $attributes Attributes for the anchor tag. */ $attr = apply_filters( 'next_comments_link_attributes', '' ); return sprintf( '%3$s', esc_url( get_comments_pagenum_link( $next_page, $max_page ) ), $attr, preg_replace( '/&([^#])(?![a-z]{1,8};)/i', '&$1', $label ) ); } /** * Displays the link to the next comments page. * * @since 2.7.0 * * @param string $label Optional. Label for link text. Default empty. * @param int $max_page Optional. Max page. Default 0. */ function next_comments_link( $label = '', $max_page = 0 ) { echo get_next_comments_link( $label, $max_page ); } /** * Retrieves the link to the previous comments page. * * @since 2.7.1 * * @param string $label Optional. Label for comments link text. Default empty. * @return string|void HTML-formatted link for the previous page of comments. */ function get_previous_comments_link( $label = '' ) { if ( ! is_singular() ) { return; } $page = get_query_var( 'cpage' ); if ( (int) $page <= 1 ) { return; } $previous_page = (int) $page - 1; if ( empty( $label ) ) { $label = __( '« Older Comments' ); } /** * Filters the anchor tag attributes for the previous comments page link. * * @since 2.7.0 * * @param string $attributes Attributes for the anchor tag. */ $attr = apply_filters( 'previous_comments_link_attributes', '' ); return sprintf( '%3$s', esc_url( get_comments_pagenum_link( $previous_page ) ), $attr, preg_replace( '/&([^#])(?![a-z]{1,8};)/i', '&$1', $label ) ); } /** * Displays the link to the previous comments page. * * @since 2.7.0 * * @param string $label Optional. Label for comments link text. Default empty. */ function previous_comments_link( $label = '' ) { echo get_previous_comments_link( $label ); } /** * Displays or retrieves pagination links for the comments on the current post. * * @see paginate_links() * @since 2.7.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param string|array $args Optional args. See paginate_links(). Default empty array. * @return void|string|array Void if 'echo' argument is true and 'type' is not an array, * or if the query is not for an existing single post of any post type. * Otherwise, markup for comment page links or array of comment page links, * depending on 'type' argument. */ function paginate_comments_links( $args = array() ) { global $wp_rewrite; if ( ! is_singular() ) { return; } $page = get_query_var( 'cpage' ); if ( ! $page ) { $page = 1; } $max_page = get_comment_pages_count(); $defaults = array( 'base' => add_query_arg( 'cpage', '%#%' ), 'format' => '', 'total' => $max_page, 'current' => $page, 'echo' => true, 'type' => 'plain', 'add_fragment' => '#comments', ); if ( $wp_rewrite->using_permalinks() ) { $defaults['base'] = user_trailingslashit( trailingslashit( get_permalink() ) . $wp_rewrite->comments_pagination_base . '-%#%', 'commentpaged' ); } $args = wp_parse_args( $args, $defaults ); $page_links = paginate_links( $args ); if ( $args['echo'] && 'array' !== $args['type'] ) { echo $page_links; } else { return $page_links; } } /** * Retrieves navigation to next/previous set of comments, when applicable. * * @since 4.4.0 * @since 5.3.0 Added the `aria_label` parameter. * @since 5.5.0 Added the `class` parameter. * * @param array $args { * Optional. Default comments navigation arguments. * * @type string $prev_text Anchor text to display in the previous comments link. * Default 'Older comments'. * @type string $next_text Anchor text to display in the next comments link. * Default 'Newer comments'. * @type string $screen_reader_text Screen reader text for the nav element. Default 'Comments navigation'. * @type string $aria_label ARIA label text for the nav element. Default 'Comments'. * @type string $class Custom class for the nav element. Default 'comment-navigation'. * } * @return string Markup for comments links. */ function get_the_comments_navigation( $args = array() ) { $navigation = ''; // Are there comments to navigate through? if ( get_comment_pages_count() > 1 ) { // Make sure the nav element has an aria-label attribute: fallback to the screen reader text. if ( ! empty( $args['screen_reader_text'] ) && empty( $args['aria_label'] ) ) { $args['aria_label'] = $args['screen_reader_text']; } $args = wp_parse_args( $args, array( 'prev_text' => __( 'Older comments' ), 'next_text' => __( 'Newer comments' ), 'screen_reader_text' => __( 'Comments navigation' ), 'aria_label' => __( 'Comments' ), 'class' => 'comment-navigation', ) ); $prev_link = get_previous_comments_link( $args['prev_text'] ); $next_link = get_next_comments_link( $args['next_text'] ); if ( $prev_link ) { $navigation .= ''; } if ( $next_link ) { $navigation .= ''; } $navigation = _navigation_markup( $navigation, $args['class'], $args['screen_reader_text'], $args['aria_label'] ); } return $navigation; } /** * Displays navigation to next/previous set of comments, when applicable. * * @since 4.4.0 * * @param array $args See get_the_comments_navigation() for available arguments. Default empty array. */ function the_comments_navigation( $args = array() ) { echo get_the_comments_navigation( $args ); } /** * Retrieves a paginated navigation to next/previous set of comments, when applicable. * * @since 4.4.0 * @since 5.3.0 Added the `aria_label` parameter. * @since 5.5.0 Added the `class` parameter. * * @see paginate_comments_links() * * @param array $args { * Optional. Default pagination arguments. * * @type string $screen_reader_text Screen reader text for the nav element. Default 'Comments navigation'. * @type string $aria_label ARIA label text for the nav element. Default 'Comments'. * @type string $class Custom class for the nav element. Default 'comments-pagination'. * } * @return string Markup for pagination links. */ function get_the_comments_pagination( $args = array() ) { $navigation = ''; // Make sure the nav element has an aria-label attribute: fallback to the screen reader text. if ( ! empty( $args['screen_reader_text'] ) && empty( $args['aria_label'] ) ) { $args['aria_label'] = $args['screen_reader_text']; } $args = wp_parse_args( $args, array( 'screen_reader_text' => __( 'Comments navigation' ), 'aria_label' => __( 'Comments' ), 'class' => 'comments-pagination', ) ); $args['echo'] = false; // Make sure we get a string back. Plain is the next best thing. if ( isset( $args['type'] ) && 'array' === $args['type'] ) { $args['type'] = 'plain'; } $links = paginate_comments_links( $args ); if ( $links ) { $navigation = _navigation_markup( $links, $args['class'], $args['screen_reader_text'], $args['aria_label'] ); } return $navigation; } /** * Displays a paginated navigation to next/previous set of comments, when applicable. * * @since 4.4.0 * * @param array $args See get_the_comments_pagination() for available arguments. Default empty array. */ function the_comments_pagination( $args = array() ) { echo get_the_comments_pagination( $args ); } /** * Retrieves the URL for the current site where the front end is accessible. * * Returns the 'home' option with the appropriate protocol. The protocol will be 'https' * if is_ssl() evaluates to true; otherwise, it will be the same as the 'home' option. * If `$scheme` is 'http' or 'https', is_ssl() is overridden. * * @since 3.0.0 * * @param string $path Optional. Path relative to the home URL. Default empty. * @param string|null $scheme Optional. Scheme to give the home URL context. Accepts * 'http', 'https', 'relative', 'rest', or null. Default null. * @return string Home URL link with optional path appended. */ function home_url( $path = '', $scheme = null ) { return get_home_url( null, $path, $scheme ); } /** * Retrieves the URL for a given site where the front end is accessible. * * Returns the 'home' option with the appropriate protocol. The protocol will be 'https' * if is_ssl() evaluates to true; otherwise, it will be the same as the 'home' option. * If `$scheme` is 'http' or 'https', is_ssl() is overridden. * * @since 3.0.0 * * @param int|null $blog_id Optional. Site ID. Default null (current site). * @param string $path Optional. Path relative to the home URL. Default empty. * @param string|null $scheme Optional. Scheme to give the home URL context. Accepts * 'http', 'https', 'relative', 'rest', or null. Default null. * @return string Home URL link with optional path appended. */ function get_home_url( $blog_id = null, $path = '', $scheme = null ) { $orig_scheme = $scheme; if ( empty( $blog_id ) || ! is_multisite() ) { $url = get_option( 'home' ); } else { switch_to_blog( $blog_id ); $url = get_option( 'home' ); restore_current_blog(); } if ( ! in_array( $scheme, array( 'http', 'https', 'relative' ), true ) ) { if ( is_ssl() ) { $scheme = 'https'; } else { $scheme = parse_url( $url, PHP_URL_SCHEME ); } } $url = set_url_scheme( $url, $scheme ); if ( $path && is_string( $path ) ) { $url .= '/' . ltrim( $path, '/' ); } /** * Filters the home URL. * * @since 3.0.0 * * @param string $url The complete home URL including scheme and path. * @param string $path Path relative to the home URL. Blank string if no path is specified. * @param string|null $orig_scheme Scheme to give the home URL context. Accepts 'http', 'https', * 'relative', 'rest', or null. * @param int|null $blog_id Site ID, or null for the current site. */ return apply_filters( 'home_url', $url, $path, $orig_scheme, $blog_id ); } /** * Retrieves the URL for the current site where WordPress application files * (e.g. wp-blog-header.php or the wp-admin/ folder) are accessible. * * Returns the 'site_url' option with the appropriate protocol, 'https' if * is_ssl() and 'http' otherwise. If $scheme is 'http' or 'https', is_ssl() is * overridden. * * @since 3.0.0 * * @param string $path Optional. Path relative to the site URL. Default empty. * @param string|null $scheme Optional. Scheme to give the site URL context. See set_url_scheme(). * @return string Site URL link with optional path appended. */ function site_url( $path = '', $scheme = null ) { return get_site_url( null, $path, $scheme ); } /** * Retrieves the URL for a given site where WordPress application files * (e.g. wp-blog-header.php or the wp-admin/ folder) are accessible. * * Returns the 'site_url' option with the appropriate protocol, 'https' if * is_ssl() and 'http' otherwise. If `$scheme` is 'http' or 'https', * `is_ssl()` is overridden. * * @since 3.0.0 * * @param int|null $blog_id Optional. Site ID. Default null (current site). * @param string $path Optional. Path relative to the site URL. Default empty. * @param string|null $scheme Optional. Scheme to give the site URL context. Accepts * 'http', 'https', 'login', 'login_post', 'admin', or * 'relative'. Default null. * @return string Site URL link with optional path appended. */ function get_site_url( $blog_id = null, $path = '', $scheme = null ) { if ( empty( $blog_id ) || ! is_multisite() ) { $url = get_option( 'siteurl' ); } else { switch_to_blog( $blog_id ); $url = get_option( 'siteurl' ); restore_current_blog(); } $url = set_url_scheme( $url, $scheme ); if ( $path && is_string( $path ) ) { $url .= '/' . ltrim( $path, '/' ); } /** * Filters the site URL. * * @since 2.7.0 * * @param string $url The complete site URL including scheme and path. * @param string $path Path relative to the site URL. Blank string if no path is specified. * @param string|null $scheme Scheme to give the site URL context. Accepts 'http', 'https', 'login', * 'login_post', 'admin', 'relative' or null. * @param int|null $blog_id Site ID, or null for the current site. */ return apply_filters( 'site_url', $url, $path, $scheme, $blog_id ); } /** * Retrieves the URL to the admin area for the current site. * * @since 2.6.0 * * @param string $path Optional. Path relative to the admin URL. Default empty. * @param string $scheme The scheme to use. Default is 'admin', which obeys force_ssl_admin() and is_ssl(). * 'http' or 'https' can be passed to force those schemes. * @return string Admin URL link with optional path appended. */ function admin_url( $path = '', $scheme = 'admin' ) { return get_admin_url( null, $path, $scheme ); } /** * Retrieves the URL to the admin area for a given site. * * @since 3.0.0 * * @param int|null $blog_id Optional. Site ID. Default null (current site). * @param string $path Optional. Path relative to the admin URL. Default empty. * @param string $scheme Optional. The scheme to use. Accepts 'http' or 'https', * to force those schemes. Default 'admin', which obeys * force_ssl_admin() and is_ssl(). * @return string Admin URL link with optional path appended. */ function get_admin_url( $blog_id = null, $path = '', $scheme = 'admin' ) { $url = get_site_url( $blog_id, 'wp-admin/', $scheme ); if ( $path && is_string( $path ) ) { $url .= ltrim( $path, '/' ); } /** * Filters the admin area URL. * * @since 2.8.0 * @since 5.8.0 The `$scheme` parameter was added. * * @param string $url The complete admin area URL including scheme and path. * @param string $path Path relative to the admin area URL. Blank string if no path is specified. * @param int|null $blog_id Site ID, or null for the current site. * @param string|null $scheme The scheme to use. Accepts 'http', 'https', * 'admin', or null. Default 'admin', which obeys force_ssl_admin() and is_ssl(). */ return apply_filters( 'admin_url', $url, $path, $blog_id, $scheme ); } /** * Retrieves the URL to the includes directory. * * @since 2.6.0 * * @param string $path Optional. Path relative to the includes URL. Default empty. * @param string|null $scheme Optional. Scheme to give the includes URL context. Accepts * 'http', 'https', or 'relative'. Default null. * @return string Includes URL link with optional path appended. */ function includes_url( $path = '', $scheme = null ) { $url = site_url( '/' . WPINC . '/', $scheme ); if ( $path && is_string( $path ) ) { $url .= ltrim( $path, '/' ); } /** * Filters the URL to the includes directory. * * @since 2.8.0 * @since 5.8.0 The `$scheme` parameter was added. * * @param string $url The complete URL to the includes directory including scheme and path. * @param string $path Path relative to the URL to the wp-includes directory. Blank string * if no path is specified. * @param string|null $scheme Scheme to give the includes URL context. Accepts * 'http', 'https', 'relative', or null. Default null. */ return apply_filters( 'includes_url', $url, $path, $scheme ); } /** * Retrieves the URL to the content directory. * * @since 2.6.0 * * @param string $path Optional. Path relative to the content URL. Default empty. * @return string Content URL link with optional path appended. */ function content_url( $path = '' ) { $url = set_url_scheme( WP_CONTENT_URL ); if ( $path && is_string( $path ) ) { $url .= '/' . ltrim( $path, '/' ); } /** * Filters the URL to the content directory. * * @since 2.8.0 * * @param string $url The complete URL to the content directory including scheme and path. * @param string $path Path relative to the URL to the content directory. Blank string * if no path is specified. */ return apply_filters( 'content_url', $url, $path ); } /** * Retrieves a URL within the plugins or mu-plugins directory. * * Defaults to the plugins directory URL if no arguments are supplied. * * @since 2.6.0 * * @param string $path Optional. Extra path appended to the end of the URL, including * the relative directory if $plugin is supplied. Default empty. * @param string $plugin Optional. A full path to a file inside a plugin or mu-plugin. * The URL will be relative to its directory. Default empty. * Typically this is done by passing `__FILE__` as the argument. * @return string Plugins URL link with optional paths appended. */ function plugins_url( $path = '', $plugin = '' ) { $path = wp_normalize_path( $path ); $plugin = wp_normalize_path( $plugin ); $mu_plugin_dir = wp_normalize_path( WPMU_PLUGIN_DIR ); if ( ! empty( $plugin ) && str_starts_with( $plugin, $mu_plugin_dir ) ) { $url = WPMU_PLUGIN_URL; } else { $url = WP_PLUGIN_URL; } $url = set_url_scheme( $url ); if ( ! empty( $plugin ) && is_string( $plugin ) ) { $folder = dirname( plugin_basename( $plugin ) ); if ( '.' !== $folder ) { $url .= '/' . ltrim( $folder, '/' ); } } if ( $path && is_string( $path ) ) { $url .= '/' . ltrim( $path, '/' ); } /** * Filters the URL to the plugins directory. * * @since 2.8.0 * * @param string $url The complete URL to the plugins directory including scheme and path. * @param string $path Path relative to the URL to the plugins directory. Blank string * if no path is specified. * @param string $plugin The plugin file path to be relative to. Blank string if no plugin * is specified. */ return apply_filters( 'plugins_url', $url, $path, $plugin ); } /** * Retrieves the site URL for the current network. * * Returns the site URL with the appropriate protocol, 'https' if * is_ssl() and 'http' otherwise. If $scheme is 'http' or 'https', is_ssl() is * overridden. * * @since 3.0.0 * * @see set_url_scheme() * * @param string $path Optional. Path relative to the site URL. Default empty. * @param string|null $scheme Optional. Scheme to give the site URL context. Accepts * 'http', 'https', or 'relative'. Default null. * @return string Site URL link with optional path appended. */ function network_site_url( $path = '', $scheme = null ) { if ( ! is_multisite() ) { return site_url( $path, $scheme ); } $current_network = get_network(); if ( 'relative' === $scheme ) { $url = $current_network->path; } else { $url = set_url_scheme( 'http://' . $current_network->domain . $current_network->path, $scheme ); } if ( $path && is_string( $path ) ) { $url .= ltrim( $path, '/' ); } /** * Filters the network site URL. * * @since 3.0.0 * * @param string $url The complete network site URL including scheme and path. * @param string $path Path relative to the network site URL. Blank string if * no path is specified. * @param string|null $scheme Scheme to give the URL context. Accepts 'http', 'https', * 'relative' or null. */ return apply_filters( 'network_site_url', $url, $path, $scheme ); } /** * Retrieves the home URL for the current network. * * Returns the home URL with the appropriate protocol, 'https' is_ssl() * and 'http' otherwise. If `$scheme` is 'http' or 'https', `is_ssl()` is * overridden. * * @since 3.0.0 * * @param string $path Optional. Path relative to the home URL. Default empty. * @param string|null $scheme Optional. Scheme to give the home URL context. Accepts * 'http', 'https', or 'relative'. Default null. * @return string Home URL link with optional path appended. */ function network_home_url( $path = '', $scheme = null ) { if ( ! is_multisite() ) { return home_url( $path, $scheme ); } $current_network = get_network(); $orig_scheme = $scheme; if ( ! in_array( $scheme, array( 'http', 'https', 'relative' ), true ) ) { $scheme = is_ssl() ? 'https' : 'http'; } if ( 'relative' === $scheme ) { $url = $current_network->path; } else { $url = set_url_scheme( 'http://' . $current_network->domain . $current_network->path, $scheme ); } if ( $path && is_string( $path ) ) { $url .= ltrim( $path, '/' ); } /** * Filters the network home URL. * * @since 3.0.0 * * @param string $url The complete network home URL including scheme and path. * @param string $path Path relative to the network home URL. Blank string * if no path is specified. * @param string|null $orig_scheme Scheme to give the URL context. Accepts 'http', 'https', * 'relative' or null. */ return apply_filters( 'network_home_url', $url, $path, $orig_scheme ); } /** * Retrieves the URL to the admin area for the network. * * @since 3.0.0 * * @param string $path Optional path relative to the admin URL. Default empty. * @param string $scheme Optional. The scheme to use. Default is 'admin', which obeys force_ssl_admin() * and is_ssl(). 'http' or 'https' can be passed to force those schemes. * @return string Admin URL link with optional path appended. */ function network_admin_url( $path = '', $scheme = 'admin' ) { if ( ! is_multisite() ) { return admin_url( $path, $scheme ); } $url = network_site_url( 'wp-admin/network/', $scheme ); if ( $path && is_string( $path ) ) { $url .= ltrim( $path, '/' ); } /** * Filters the network admin URL. * * @since 3.0.0 * @since 5.8.0 The `$scheme` parameter was added. * * @param string $url The complete network admin URL including scheme and path. * @param string $path Path relative to the network admin URL. Blank string if * no path is specified. * @param string|null $scheme The scheme to use. Accepts 'http', 'https', * 'admin', or null. Default is 'admin', which obeys force_ssl_admin() and is_ssl(). */ return apply_filters( 'network_admin_url', $url, $path, $scheme ); } /** * Retrieves the URL to the admin area for the current user. * * @since 3.0.0 * * @param string $path Optional. Path relative to the admin URL. Default empty. * @param string $scheme Optional. The scheme to use. Default is 'admin', which obeys force_ssl_admin() * and is_ssl(). 'http' or 'https' can be passed to force those schemes. * @return string Admin URL link with optional path appended. */ function user_admin_url( $path = '', $scheme = 'admin' ) { $url = network_site_url( 'wp-admin/user/', $scheme ); if ( $path && is_string( $path ) ) { $url .= ltrim( $path, '/' ); } /** * Filters the user admin URL for the current user. * * @since 3.1.0 * @since 5.8.0 The `$scheme` parameter was added. * * @param string $url The complete URL including scheme and path. * @param string $path Path relative to the URL. Blank string if * no path is specified. * @param string|null $scheme The scheme to use. Accepts 'http', 'https', * 'admin', or null. Default is 'admin', which obeys force_ssl_admin() and is_ssl(). */ return apply_filters( 'user_admin_url', $url, $path, $scheme ); } /** * Retrieves the URL to the admin area for either the current site or the network depending on context. * * @since 3.1.0 * * @param string $path Optional. Path relative to the admin URL. Default empty. * @param string $scheme Optional. The scheme to use. Default is 'admin', which obeys force_ssl_admin() * and is_ssl(). 'http' or 'https' can be passed to force those schemes. * @return string Admin URL link with optional path appended. */ function self_admin_url( $path = '', $scheme = 'admin' ) { if ( is_network_admin() ) { $url = network_admin_url( $path, $scheme ); } elseif ( is_user_admin() ) { $url = user_admin_url( $path, $scheme ); } else { $url = admin_url( $path, $scheme ); } /** * Filters the admin URL for the current site or network depending on context. * * @since 4.9.0 * * @param string $url The complete URL including scheme and path. * @param string $path Path relative to the URL. Blank string if no path is specified. * @param string $scheme The scheme to use. */ return apply_filters( 'self_admin_url', $url, $path, $scheme ); } /** * Sets the scheme for a URL. * * @since 3.4.0 * @since 4.4.0 The 'rest' scheme was added. * * @param string $url Absolute URL that includes a scheme * @param string|null $scheme Optional. Scheme to give $url. Currently 'http', 'https', 'login', * 'login_post', 'admin', 'relative', 'rest', 'rpc', or null. Default null. * @return string URL with chosen scheme. */ function set_url_scheme( $url, $scheme = null ) { $orig_scheme = $scheme; if ( ! $scheme ) { $scheme = is_ssl() ? 'https' : 'http'; } elseif ( 'admin' === $scheme || 'login' === $scheme || 'login_post' === $scheme || 'rpc' === $scheme ) { $scheme = is_ssl() || force_ssl_admin() ? 'https' : 'http'; } elseif ( 'http' !== $scheme && 'https' !== $scheme && 'relative' !== $scheme ) { $scheme = is_ssl() ? 'https' : 'http'; } $url = trim( $url ); if ( str_starts_with( $url, '//' ) ) { $url = 'http:' . $url; } if ( 'relative' === $scheme ) { $url = ltrim( preg_replace( '#^\w+://[^/]*#', '', $url ) ); if ( '' !== $url && '/' === $url[0] ) { $url = '/' . ltrim( $url, "/ \t\n\r\0\x0B" ); } } else { $url = preg_replace( '#^\w+://#', $scheme . '://', $url ); } /** * Filters the resulting URL after setting the scheme. * * @since 3.4.0 * * @param string $url The complete URL including scheme and path. * @param string $scheme Scheme applied to the URL. One of 'http', 'https', or 'relative'. * @param string|null $orig_scheme Scheme requested for the URL. One of 'http', 'https', 'login', * 'login_post', 'admin', 'relative', 'rest', 'rpc', or null. */ return apply_filters( 'set_url_scheme', $url, $scheme, $orig_scheme ); } /** * Retrieves the URL to the user's dashboard. * * If a user does not belong to any site, the global user dashboard is used. If the user * belongs to the current site, the dashboard for the current site is returned. If the user * cannot edit the current site, the dashboard to the user's primary site is returned. * * @since 3.1.0 * * @param int $user_id Optional. User ID. Defaults to current user. * @param string $path Optional path relative to the dashboard. Use only paths known to * both site and user admins. Default empty. * @param string $scheme The scheme to use. Default is 'admin', which obeys force_ssl_admin() * and is_ssl(). 'http' or 'https' can be passed to force those schemes. * @return string Dashboard URL link with optional path appended. */ function get_dashboard_url( $user_id = 0, $path = '', $scheme = 'admin' ) { $user_id = $user_id ? (int) $user_id : get_current_user_id(); $blogs = get_blogs_of_user( $user_id ); if ( is_multisite() && ! user_can( $user_id, 'manage_network' ) && empty( $blogs ) ) { $url = user_admin_url( $path, $scheme ); } elseif ( ! is_multisite() ) { $url = admin_url( $path, $scheme ); } else { $current_blog = get_current_blog_id(); if ( $current_blog && ( user_can( $user_id, 'manage_network' ) || in_array( $current_blog, array_keys( $blogs ), true ) ) ) { $url = admin_url( $path, $scheme ); } else { $active = get_active_blog_for_user( $user_id ); if ( $active ) { $url = get_admin_url( $active->blog_id, $path, $scheme ); } else { $url = user_admin_url( $path, $scheme ); } } } /** * Filters the dashboard URL for a user. * * @since 3.1.0 * * @param string $url The complete URL including scheme and path. * @param int $user_id The user ID. * @param string $path Path relative to the URL. Blank string if no path is specified. * @param string $scheme Scheme to give the URL context. Accepts 'http', 'https', 'login', * 'login_post', 'admin', 'relative' or null. */ return apply_filters( 'user_dashboard_url', $url, $user_id, $path, $scheme ); } /** * Retrieves the URL to the user's profile editor. * * @since 3.1.0 * * @param int $user_id Optional. User ID. Defaults to current user. * @param string $scheme Optional. The scheme to use. Default is 'admin', which obeys force_ssl_admin() * and is_ssl(). 'http' or 'https' can be passed to force those schemes. * @return string Dashboard URL link with optional path appended. */ function get_edit_profile_url( $user_id = 0, $scheme = 'admin' ) { $user_id = $user_id ? (int) $user_id : get_current_user_id(); if ( is_user_admin() ) { $url = user_admin_url( 'profile.php', $scheme ); } elseif ( is_network_admin() ) { $url = network_admin_url( 'profile.php', $scheme ); } else { $url = get_dashboard_url( $user_id, 'profile.php', $scheme ); } /** * Filters the URL for a user's profile editor. * * @since 3.1.0 * * @param string $url The complete URL including scheme and path. * @param int $user_id The user ID. * @param string $scheme Scheme to give the URL context. Accepts 'http', 'https', 'login', * 'login_post', 'admin', 'relative' or null. */ return apply_filters( 'edit_profile_url', $url, $user_id, $scheme ); } /** * Returns the canonical URL for a post. * * When the post is the same as the current requested page the function will handle the * pagination arguments too. * * @since 4.6.0 * * @param int|WP_Post $post Optional. Post ID or object. Default is global `$post`. * @return string|false The canonical URL. False if the post does not exist * or has not been published yet. */ function wp_get_canonical_url( $post = null ) { $post = get_post( $post ); if ( ! $post ) { return false; } if ( 'publish' !== $post->post_status ) { return false; } $canonical_url = get_permalink( $post ); // If a canonical is being generated for the current page, make sure it has pagination if needed. if ( get_queried_object_id() === $post->ID ) { $page = get_query_var( 'page', 0 ); if ( $page >= 2 ) { if ( ! get_option( 'permalink_structure' ) ) { $canonical_url = add_query_arg( 'page', $page, $canonical_url ); } else { $canonical_url = trailingslashit( $canonical_url ) . user_trailingslashit( $page, 'single_paged' ); } } $cpage = get_query_var( 'cpage', 0 ); if ( $cpage ) { $canonical_url = get_comments_pagenum_link( $cpage ); } } /** * Filters the canonical URL for a post. * * @since 4.6.0 * * @param string $canonical_url The post's canonical URL. * @param WP_Post $post Post object. */ return apply_filters( 'get_canonical_url', $canonical_url, $post ); } /** * Outputs rel=canonical for singular queries. * * @since 2.9.0 * @since 4.6.0 Adjusted to use `wp_get_canonical_url()`. */ function rel_canonical() { if ( ! is_singular() ) { return; } $id = get_queried_object_id(); if ( 0 === $id ) { return; } $url = wp_get_canonical_url( $id ); if ( ! empty( $url ) ) { echo '' . "\n"; } } /** * Returns a shortlink for a post, page, attachment, or site. * * This function exists to provide a shortlink tag that all themes and plugins can target. * A plugin must hook in to provide the actual shortlinks. Default shortlink support is * limited to providing ?p= style links for posts. Plugins can short-circuit this function * via the {@see 'pre_get_shortlink'} filter or filter the output via the {@see 'get_shortlink'} * filter. * * @since 3.0.0 * * @param int $id Optional. A post or site ID. Default is 0, which means the current post or site. * @param string $context Optional. Whether the ID is a 'site' ID, 'post' ID, or 'media' ID. If 'post', * the post_type of the post is consulted. If 'query', the current query is consulted * to determine the ID and context. Default 'post'. * @param bool $allow_slugs Optional. Whether to allow post slugs in the shortlink. It is up to the plugin how * and whether to honor this. Default true. * @return string A shortlink or an empty string if no shortlink exists for the requested resource or if shortlinks * are not enabled. */ function wp_get_shortlink( $id = 0, $context = 'post', $allow_slugs = true ) { /** * Filters whether to preempt generating a shortlink for the given post. * * Returning a value other than false from the filter will short-circuit * the shortlink generation process, returning that value instead. * * @since 3.0.0 * * @param false|string $return Short-circuit return value. Either false or a URL string. * @param int $id Post ID, or 0 for the current post. * @param string $context The context for the link. One of 'post' or 'query', * @param bool $allow_slugs Whether to allow post slugs in the shortlink. */ $shortlink = apply_filters( 'pre_get_shortlink', false, $id, $context, $allow_slugs ); if ( false !== $shortlink ) { return $shortlink; } $post_id = 0; if ( 'query' === $context && is_singular() ) { $post_id = get_queried_object_id(); $post = get_post( $post_id ); } elseif ( 'post' === $context ) { $post = get_post( $id ); if ( ! empty( $post->ID ) ) { $post_id = $post->ID; } } $shortlink = ''; // Return `?p=` link for all public post types. if ( ! empty( $post_id ) ) { $post_type = get_post_type_object( $post->post_type ); if ( 'page' === $post->post_type && get_option( 'page_on_front' ) == $post->ID && 'page' === get_option( 'show_on_front' ) ) { $shortlink = home_url( '/' ); } elseif ( $post_type && $post_type->public ) { $shortlink = home_url( '?p=' . $post_id ); } } /** * Filters the shortlink for a post. * * @since 3.0.0 * * @param string $shortlink Shortlink URL. * @param int $id Post ID, or 0 for the current post. * @param string $context The context for the link. One of 'post' or 'query', * @param bool $allow_slugs Whether to allow post slugs in the shortlink. Not used by default. */ return apply_filters( 'get_shortlink', $shortlink, $id, $context, $allow_slugs ); } /** * Injects rel=shortlink into the head if a shortlink is defined for the current page. * * Attached to the {@see 'wp_head'} action. * * @since 3.0.0 */ function wp_shortlink_wp_head() { $shortlink = wp_get_shortlink( 0, 'query' ); if ( empty( $shortlink ) ) { return; } echo "\n"; } /** * Sends a Link: rel=shortlink header if a shortlink is defined for the current page. * * Attached to the {@see 'wp'} action. * * @since 3.0.0 */ function wp_shortlink_header() { if ( headers_sent() ) { return; } $shortlink = wp_get_shortlink( 0, 'query' ); if ( empty( $shortlink ) ) { return; } header( 'Link: <' . $shortlink . '>; rel=shortlink', false ); } /** * Displays the shortlink for a post. * * Must be called from inside "The Loop" * * Call like the_shortlink( __( 'Shortlinkage FTW' ) ) * * @since 3.0.0 * * @param string $text Optional The link text or HTML to be displayed. Defaults to 'This is the short link.' * @param string $title Optional The tooltip for the link. Must be sanitized. Defaults to the sanitized post title. * @param string $before Optional HTML to display before the link. Default empty. * @param string $after Optional HTML to display after the link. Default empty. */ function the_shortlink( $text = '', $title = '', $before = '', $after = '' ) { $post = get_post(); if ( empty( $text ) ) { $text = __( 'This is the short link.' ); } if ( empty( $title ) ) { $title = the_title_attribute( array( 'echo' => false ) ); } $shortlink = wp_get_shortlink( $post->ID ); if ( ! empty( $shortlink ) ) { $link = '' . $text . ''; /** * Filters the short link anchor tag for a post. * * @since 3.0.0 * * @param string $link Shortlink anchor tag. * @param string $shortlink Shortlink URL. * @param string $text Shortlink's text. * @param string $title Shortlink's title attribute. */ $link = apply_filters( 'the_shortlink', $link, $shortlink, $text, $title ); echo $before, $link, $after; } } /** * Retrieves the avatar URL. * * @since 4.2.0 * * @param mixed $id_or_email The avatar to retrieve a URL for. Accepts a user ID, Gravatar MD5 hash, * user email, WP_User object, WP_Post object, or WP_Comment object. * @param array $args { * Optional. Arguments to use instead of the default arguments. * * @type int $size Height and width of the avatar in pixels. Default 96. * @type string $default URL for the default image or a default type. Accepts: * - '404' (return a 404 instead of a default image) * - 'retro' (a 8-bit arcade-style pixelated face) * - 'robohash' (a robot) * - 'monsterid' (a monster) * - 'wavatar' (a cartoon face) * - 'identicon' (the "quilt", a geometric pattern) * - 'mystery', 'mm', or 'mysteryman' (The Oyster Man) * - 'blank' (transparent GIF) * - 'gravatar_default' (the Gravatar logo) * Default is the value of the 'avatar_default' option, * with a fallback of 'mystery'. * @type bool $force_default Whether to always show the default image, never the Gravatar. * Default false. * @type string $rating What rating to display avatars up to. Accepts: * - 'G' (suitable for all audiences) * - 'PG' (possibly offensive, usually for audiences 13 and above) * - 'R' (intended for adult audiences above 17) * - 'X' (even more mature than above) * Default is the value of the 'avatar_rating' option. * @type string $scheme URL scheme to use. See set_url_scheme() for accepted values. * Default null. * @type array $processed_args When the function returns, the value will be the processed/sanitized $args * plus a "found_avatar" guess. Pass as a reference. Default null. * } * @return string|false The URL of the avatar on success, false on failure. */ function get_avatar_url( $id_or_email, $args = null ) { $args = get_avatar_data( $id_or_email, $args ); return $args['url']; } /** * Check if this comment type allows avatars to be retrieved. * * @since 5.1.0 * * @param string $comment_type Comment type to check. * @return bool Whether the comment type is allowed for retrieving avatars. */ function is_avatar_comment_type( $comment_type ) { /** * Filters the list of allowed comment types for retrieving avatars. * * @since 3.0.0 * * @param array $types An array of content types. Default only contains 'comment'. */ $allowed_comment_types = apply_filters( 'get_avatar_comment_types', array( 'comment' ) ); return in_array( $comment_type, (array) $allowed_comment_types, true ); } /** * Retrieves default data about the avatar. * * @since 4.2.0 * * @param mixed $id_or_email The avatar to retrieve. Accepts a user ID, Gravatar MD5 hash, * user email, WP_User object, WP_Post object, or WP_Comment object. * @param array $args { * Optional. Arguments to use instead of the default arguments. * * @type int $size Height and width of the avatar in pixels. Default 96. * @type int $height Display height of the avatar in pixels. Defaults to $size. * @type int $width Display width of the avatar in pixels. Defaults to $size. * @type string $default URL for the default image or a default type. Accepts: * - '404' (return a 404 instead of a default image) * - 'retro' (a 8-bit arcade-style pixelated face) * - 'robohash' (a robot) * - 'monsterid' (a monster) * - 'wavatar' (a cartoon face) * - 'identicon' (the "quilt", a geometric pattern) * - 'mystery', 'mm', or 'mysteryman' (The Oyster Man) * - 'blank' (transparent GIF) * - 'gravatar_default' (the Gravatar logo) * Default is the value of the 'avatar_default' option, * with a fallback of 'mystery'. * @type bool $force_default Whether to always show the default image, never the Gravatar. * Default false. * @type string $rating What rating to display avatars up to. Accepts: * - 'G' (suitable for all audiences) * - 'PG' (possibly offensive, usually for audiences 13 and above) * - 'R' (intended for adult audiences above 17) * - 'X' (even more mature than above) * Default is the value of the 'avatar_rating' option. * @type string $scheme URL scheme to use. See set_url_scheme() for accepted values. * Default null. * @type array $processed_args When the function returns, the value will be the processed/sanitized $args * plus a "found_avatar" guess. Pass as a reference. Default null. * @type string $extra_attr HTML attributes to insert in the IMG element. Is not sanitized. * Default empty. * } * @return array { * Along with the arguments passed in `$args`, this will contain a couple of extra arguments. * * @type bool $found_avatar True if an avatar was found for this user, * false or not set if none was found. * @type string|false $url The URL of the avatar that was found, or false. * } */ function get_avatar_data( $id_or_email, $args = null ) { $args = wp_parse_args( $args, array( 'size' => 96, 'height' => null, 'width' => null, 'default' => get_option( 'avatar_default', 'mystery' ), 'force_default' => false, 'rating' => get_option( 'avatar_rating' ), 'scheme' => null, 'processed_args' => null, // If used, should be a reference. 'extra_attr' => '', ) ); if ( is_numeric( $args['size'] ) ) { $args['size'] = absint( $args['size'] ); if ( ! $args['size'] ) { $args['size'] = 96; } } else { $args['size'] = 96; } if ( is_numeric( $args['height'] ) ) { $args['height'] = absint( $args['height'] ); if ( ! $args['height'] ) { $args['height'] = $args['size']; } } else { $args['height'] = $args['size']; } if ( is_numeric( $args['width'] ) ) { $args['width'] = absint( $args['width'] ); if ( ! $args['width'] ) { $args['width'] = $args['size']; } } else { $args['width'] = $args['size']; } if ( empty( $args['default'] ) ) { $args['default'] = get_option( 'avatar_default', 'mystery' ); } switch ( $args['default'] ) { case 'mm': case 'mystery': case 'mysteryman': $args['default'] = 'mm'; break; case 'gravatar_default': $args['default'] = false; break; } $args['force_default'] = (bool) $args['force_default']; $args['rating'] = strtolower( $args['rating'] ); $args['found_avatar'] = false; /** * Filters whether to retrieve the avatar URL early. * * Passing a non-null value in the 'url' member of the return array will * effectively short circuit get_avatar_data(), passing the value through * the {@see 'get_avatar_data'} filter and returning early. * * @since 4.2.0 * * @param array $args Arguments passed to get_avatar_data(), after processing. * @param mixed $id_or_email The avatar to retrieve. Accepts a user ID, Gravatar MD5 hash, * user email, WP_User object, WP_Post object, or WP_Comment object. */ $args = apply_filters( 'pre_get_avatar_data', $args, $id_or_email ); if ( isset( $args['url'] ) ) { /** This filter is documented in wp-includes/link-template.php */ return apply_filters( 'get_avatar_data', $args, $id_or_email ); } $email_hash = ''; $user = false; $email = false; if ( is_object( $id_or_email ) && isset( $id_or_email->comment_ID ) ) { $id_or_email = get_comment( $id_or_email ); } // Process the user identifier. if ( is_numeric( $id_or_email ) ) { $user = get_user_by( 'id', absint( $id_or_email ) ); } elseif ( is_string( $id_or_email ) ) { if ( str_contains( $id_or_email, '@md5.gravatar.com' ) ) { // MD5 hash. list( $email_hash ) = explode( '@', $id_or_email ); } else { // Email address. $email = $id_or_email; } } elseif ( $id_or_email instanceof WP_User ) { // User object. $user = $id_or_email; } elseif ( $id_or_email instanceof WP_Post ) { // Post object. $user = get_user_by( 'id', (int) $id_or_email->post_author ); } elseif ( $id_or_email instanceof WP_Comment ) { if ( ! is_avatar_comment_type( get_comment_type( $id_or_email ) ) ) { $args['url'] = false; /** This filter is documented in wp-includes/link-template.php */ return apply_filters( 'get_avatar_data', $args, $id_or_email ); } if ( ! empty( $id_or_email->user_id ) ) { $user = get_user_by( 'id', (int) $id_or_email->user_id ); } if ( ( ! $user || is_wp_error( $user ) ) && ! empty( $id_or_email->comment_author_email ) ) { $email = $id_or_email->comment_author_email; } } if ( ! $email_hash ) { if ( $user ) { $email = $user->user_email; } if ( $email ) { $email_hash = md5( strtolower( trim( $email ) ) ); } } if ( $email_hash ) { $args['found_avatar'] = true; $gravatar_server = hexdec( $email_hash[0] ) % 3; } else { $gravatar_server = rand( 0, 2 ); } $url_args = array( 's' => $args['size'], 'd' => $args['default'], 'f' => $args['force_default'] ? 'y' : false, 'r' => $args['rating'], ); if ( is_ssl() ) { $url = 'https://secure.gravatar.com/avatar/' . $email_hash; } else { $url = sprintf( 'http://%d.gravatar.com/avatar/%s', $gravatar_server, $email_hash ); } $url = add_query_arg( rawurlencode_deep( array_filter( $url_args ) ), set_url_scheme( $url, $args['scheme'] ) ); /** * Filters the avatar URL. * * @since 4.2.0 * * @param string $url The URL of the avatar. * @param mixed $id_or_email The avatar to retrieve. Accepts a user ID, Gravatar MD5 hash, * user email, WP_User object, WP_Post object, or WP_Comment object. * @param array $args Arguments passed to get_avatar_data(), after processing. */ $args['url'] = apply_filters( 'get_avatar_url', $url, $id_or_email, $args ); /** * Filters the avatar data. * * @since 4.2.0 * * @param array $args Arguments passed to get_avatar_data(), after processing. * @param mixed $id_or_email The avatar to retrieve. Accepts a user ID, Gravatar MD5 hash, * user email, WP_User object, WP_Post object, or WP_Comment object. */ return apply_filters( 'get_avatar_data', $args, $id_or_email ); } /** * Retrieves the URL of a file in the theme. * * Searches in the stylesheet directory before the template directory so themes * which inherit from a parent theme can just override one file. * * @since 4.7.0 * * @param string $file Optional. File to search for in the stylesheet directory. * @return string The URL of the file. */ function get_theme_file_uri( $file = '' ) { $file = ltrim( $file, '/' ); $stylesheet_directory = get_stylesheet_directory(); if ( empty( $file ) ) { $url = get_stylesheet_directory_uri(); } elseif ( get_template_directory() !== $stylesheet_directory && file_exists( $stylesheet_directory . '/' . $file ) ) { $url = get_stylesheet_directory_uri() . '/' . $file; } else { $url = get_template_directory_uri() . '/' . $file; } /** * Filters the URL to a file in the theme. * * @since 4.7.0 * * @param string $url The file URL. * @param string $file The requested file to search for. */ return apply_filters( 'theme_file_uri', $url, $file ); } /** * Retrieves the URL of a file in the parent theme. * * @since 4.7.0 * * @param string $file Optional. File to return the URL for in the template directory. * @return string The URL of the file. */ function get_parent_theme_file_uri( $file = '' ) { $file = ltrim( $file, '/' ); if ( empty( $file ) ) { $url = get_template_directory_uri(); } else { $url = get_template_directory_uri() . '/' . $file; } /** * Filters the URL to a file in the parent theme. * * @since 4.7.0 * * @param string $url The file URL. * @param string $file The requested file to search for. */ return apply_filters( 'parent_theme_file_uri', $url, $file ); } /** * Retrieves the path of a file in the theme. * * Searches in the stylesheet directory before the template directory so themes * which inherit from a parent theme can just override one file. * * @since 4.7.0 * * @param string $file Optional. File to search for in the stylesheet directory. * @return string The path of the file. */ function get_theme_file_path( $file = '' ) { $file = ltrim( $file, '/' ); $stylesheet_directory = get_stylesheet_directory(); $template_directory = get_template_directory(); if ( empty( $file ) ) { $path = $stylesheet_directory; } elseif ( $stylesheet_directory !== $template_directory && file_exists( $stylesheet_directory . '/' . $file ) ) { $path = $stylesheet_directory . '/' . $file; } else { $path = $template_directory . '/' . $file; } /** * Filters the path to a file in the theme. * * @since 4.7.0 * * @param string $path The file path. * @param string $file The requested file to search for. */ return apply_filters( 'theme_file_path', $path, $file ); } /** * Retrieves the path of a file in the parent theme. * * @since 4.7.0 * * @param string $file Optional. File to return the path for in the template directory. * @return string The path of the file. */ function get_parent_theme_file_path( $file = '' ) { $file = ltrim( $file, '/' ); if ( empty( $file ) ) { $path = get_template_directory(); } else { $path = get_template_directory() . '/' . $file; } /** * Filters the path to a file in the parent theme. * * @since 4.7.0 * * @param string $path The file path. * @param string $file The requested file to search for. */ return apply_filters( 'parent_theme_file_path', $path, $file ); } /** * Retrieves the URL to the privacy policy page. * * @since 4.9.6 * * @return string The URL to the privacy policy page. Empty string if it doesn't exist. */ function get_privacy_policy_url() { $url = ''; $policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' ); if ( ! empty( $policy_page_id ) && get_post_status( $policy_page_id ) === 'publish' ) { $url = (string) get_permalink( $policy_page_id ); } /** * Filters the URL of the privacy policy page. * * @since 4.9.6 * * @param string $url The URL to the privacy policy page. Empty string * if it doesn't exist. * @param int $policy_page_id The ID of privacy policy page. */ return apply_filters( 'privacy_policy_url', $url, $policy_page_id ); } /** * Displays the privacy policy link with formatting, when applicable. * * @since 4.9.6 * * @param string $before Optional. Display before privacy policy link. Default empty. * @param string $after Optional. Display after privacy policy link. Default empty. */ function the_privacy_policy_link( $before = '', $after = '' ) { echo get_the_privacy_policy_link( $before, $after ); } /** * Returns the privacy policy link with formatting, when applicable. * * @since 4.9.6 * @since 6.2.0 Added 'privacy-policy' rel attribute. * * @param string $before Optional. Display before privacy policy link. Default empty. * @param string $after Optional. Display after privacy policy link. Default empty. * @return string Markup for the link and surrounding elements. Empty string if it * doesn't exist. */ function get_the_privacy_policy_link( $before = '', $after = '' ) { $link = ''; $privacy_policy_url = get_privacy_policy_url(); $policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' ); $page_title = ( $policy_page_id ) ? get_the_title( $policy_page_id ) : ''; if ( $privacy_policy_url && $page_title ) { $link = sprintf( '%s', esc_url( $privacy_policy_url ), esc_html( $page_title ) ); } /** * Filters the privacy policy link. * * @since 4.9.6 * * @param string $link The privacy policy link. Empty string if it * doesn't exist. * @param string $privacy_policy_url The URL of the privacy policy. Empty string * if it doesn't exist. */ $link = apply_filters( 'the_privacy_policy_link', $link, $privacy_policy_url ); if ( $link ) { return $before . $link . $after; } return ''; } /** * Returns an array of URL hosts which are considered to be internal hosts. * * By default the list of internal hosts is comprised of the host name of * the site's home_url() (as parsed by wp_parse_url()). * * This list is used when determining if a specificed URL is a link to a page on * the site itself or a link offsite (to an external host). This is used, for * example, when determining if the "nofollow" attribute should be applied to a * link. * * @see wp_is_internal_link * * @since 6.2.0 * * @return string[] An array of URL hosts. */ function wp_internal_hosts() { static $internal_hosts; if ( empty( $internal_hosts ) ) { /** * Filters the array of URL hosts which are considered internal. * * @since 6.2.0 * * @param string[] $internal_hosts An array of internal URL hostnames. */ $internal_hosts = apply_filters( 'wp_internal_hosts', array( wp_parse_url( home_url(), PHP_URL_HOST ), ) ); $internal_hosts = array_unique( array_map( 'strtolower', (array) $internal_hosts ) ); } return $internal_hosts; } /** * Determines whether or not the specified URL is of a host included in the internal hosts list. * * @see wp_internal_hosts() * * @since 6.2.0 * * @param string $link The URL to test. * @return bool Returns true for internal URLs and false for all other URLs. */ function wp_is_internal_link( $link ) { $link = strtolower( $link ); if ( in_array( wp_parse_url( $link, PHP_URL_SCHEME ), wp_allowed_protocols(), true ) ) { return in_array( wp_parse_url( $link, PHP_URL_HOST ), wp_internal_hosts(), true ); } return false; } /** * Core Post API * * @package WordPress * @subpackage Post */ // // Post Type registration. // /** * Creates the initial post types when 'init' action is fired. * * See {@see 'init'}. * * @since 2.9.0 */ function create_initial_post_types() { WP_Post_Type::reset_default_labels(); register_post_type( 'post', array( 'labels' => array( 'name_admin_bar' => _x( 'Post', 'add new from admin bar' ), ), 'public' => true, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ '_edit_link' => 'post.php?post=%d', /* internal use only. don't use this when registering your own post type. */ 'capability_type' => 'post', 'map_meta_cap' => true, 'menu_position' => 5, 'menu_icon' => 'dashicons-admin-post', 'hierarchical' => false, 'rewrite' => false, 'query_var' => false, 'delete_with_user' => true, 'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'trackbacks', 'custom-fields', 'comments', 'revisions', 'post-formats' ), 'show_in_rest' => true, 'rest_base' => 'posts', 'rest_controller_class' => 'WP_REST_Posts_Controller', ) ); register_post_type( 'page', array( 'labels' => array( 'name_admin_bar' => _x( 'Page', 'add new from admin bar' ), ), 'public' => true, 'publicly_queryable' => false, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ '_edit_link' => 'post.php?post=%d', /* internal use only. don't use this when registering your own post type. */ 'capability_type' => 'page', 'map_meta_cap' => true, 'menu_position' => 20, 'menu_icon' => 'dashicons-admin-page', 'hierarchical' => true, 'rewrite' => false, 'query_var' => false, 'delete_with_user' => true, 'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'page-attributes', 'custom-fields', 'comments', 'revisions' ), 'show_in_rest' => true, 'rest_base' => 'pages', 'rest_controller_class' => 'WP_REST_Posts_Controller', ) ); register_post_type( 'attachment', array( 'labels' => array( 'name' => _x( 'Media', 'post type general name' ), 'name_admin_bar' => _x( 'Media', 'add new from admin bar' ), 'add_new' => __( 'Add New Media File' ), 'edit_item' => __( 'Edit Media' ), 'view_item' => ( '1' === get_option( 'wp_attachment_pages_enabled' ) ) ? __( 'View Attachment Page' ) : __( 'View Media File' ), 'attributes' => __( 'Attachment Attributes' ), ), 'public' => true, 'show_ui' => true, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ '_edit_link' => 'post.php?post=%d', /* internal use only. don't use this when registering your own post type. */ 'capability_type' => 'post', 'capabilities' => array( 'create_posts' => 'upload_files', ), 'map_meta_cap' => true, 'menu_icon' => 'dashicons-admin-media', 'hierarchical' => false, 'rewrite' => false, 'query_var' => false, 'show_in_nav_menus' => false, 'delete_with_user' => true, 'supports' => array( 'title', 'author', 'comments' ), 'show_in_rest' => true, 'rest_base' => 'media', 'rest_controller_class' => 'WP_REST_Attachments_Controller', ) ); add_post_type_support( 'attachment:audio', 'thumbnail' ); add_post_type_support( 'attachment:video', 'thumbnail' ); register_post_type( 'revision', array( 'labels' => array( 'name' => __( 'Revisions' ), 'singular_name' => __( 'Revision' ), ), 'public' => false, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ '_edit_link' => 'revision.php?revision=%d', /* internal use only. don't use this when registering your own post type. */ 'capability_type' => 'post', 'map_meta_cap' => true, 'hierarchical' => false, 'rewrite' => false, 'query_var' => false, 'can_export' => false, 'delete_with_user' => true, 'supports' => array( 'author' ), ) ); register_post_type( 'nav_menu_item', array( 'labels' => array( 'name' => __( 'Navigation Menu Items' ), 'singular_name' => __( 'Navigation Menu Item' ), ), 'public' => false, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ 'hierarchical' => false, 'rewrite' => false, 'delete_with_user' => false, 'query_var' => false, 'map_meta_cap' => true, 'capability_type' => array( 'edit_theme_options', 'edit_theme_options' ), 'capabilities' => array( // Meta Capabilities. 'edit_post' => 'edit_post', 'read_post' => 'read_post', 'delete_post' => 'delete_post', // Primitive Capabilities. 'edit_posts' => 'edit_theme_options', 'edit_others_posts' => 'edit_theme_options', 'delete_posts' => 'edit_theme_options', 'publish_posts' => 'edit_theme_options', 'read_private_posts' => 'edit_theme_options', 'read' => 'read', 'delete_private_posts' => 'edit_theme_options', 'delete_published_posts' => 'edit_theme_options', 'delete_others_posts' => 'edit_theme_options', 'edit_private_posts' => 'edit_theme_options', 'edit_published_posts' => 'edit_theme_options', ), 'show_in_rest' => true, 'rest_base' => 'menu-items', 'rest_controller_class' => 'WP_REST_Menu_Items_Controller', ) ); register_post_type( 'custom_css', array( 'labels' => array( 'name' => __( 'Custom CSS' ), 'singular_name' => __( 'Custom CSS' ), ), 'public' => false, 'hierarchical' => false, 'rewrite' => false, 'query_var' => false, 'delete_with_user' => false, 'can_export' => true, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ 'supports' => array( 'title', 'revisions' ), 'capabilities' => array( 'delete_posts' => 'edit_theme_options', 'delete_post' => 'edit_theme_options', 'delete_published_posts' => 'edit_theme_options', 'delete_private_posts' => 'edit_theme_options', 'delete_others_posts' => 'edit_theme_options', 'edit_post' => 'edit_css', 'edit_posts' => 'edit_css', 'edit_others_posts' => 'edit_css', 'edit_published_posts' => 'edit_css', 'read_post' => 'read', 'read_private_posts' => 'read', 'publish_posts' => 'edit_theme_options', ), ) ); register_post_type( 'customize_changeset', array( 'labels' => array( 'name' => _x( 'Changesets', 'post type general name' ), 'singular_name' => _x( 'Changeset', 'post type singular name' ), 'add_new' => __( 'Add New Changeset' ), 'add_new_item' => __( 'Add New Changeset' ), 'new_item' => __( 'New Changeset' ), 'edit_item' => __( 'Edit Changeset' ), 'view_item' => __( 'View Changeset' ), 'all_items' => __( 'All Changesets' ), 'search_items' => __( 'Search Changesets' ), 'not_found' => __( 'No changesets found.' ), 'not_found_in_trash' => __( 'No changesets found in Trash.' ), ), 'public' => false, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ 'map_meta_cap' => true, 'hierarchical' => false, 'rewrite' => false, 'query_var' => false, 'can_export' => false, 'delete_with_user' => false, 'supports' => array( 'title', 'author' ), 'capability_type' => 'customize_changeset', 'capabilities' => array( 'create_posts' => 'customize', 'delete_others_posts' => 'customize', 'delete_post' => 'customize', 'delete_posts' => 'customize', 'delete_private_posts' => 'customize', 'delete_published_posts' => 'customize', 'edit_others_posts' => 'customize', 'edit_post' => 'customize', 'edit_posts' => 'customize', 'edit_private_posts' => 'customize', 'edit_published_posts' => 'do_not_allow', 'publish_posts' => 'customize', 'read' => 'read', 'read_post' => 'customize', 'read_private_posts' => 'customize', ), ) ); register_post_type( 'oembed_cache', array( 'labels' => array( 'name' => __( 'oEmbed Responses' ), 'singular_name' => __( 'oEmbed Response' ), ), 'public' => false, 'hierarchical' => false, 'rewrite' => false, 'query_var' => false, 'delete_with_user' => false, 'can_export' => false, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ 'supports' => array(), ) ); register_post_type( 'user_request', array( 'labels' => array( 'name' => __( 'User Requests' ), 'singular_name' => __( 'User Request' ), ), 'public' => false, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ 'hierarchical' => false, 'rewrite' => false, 'query_var' => false, 'can_export' => false, 'delete_with_user' => false, 'supports' => array(), ) ); register_post_type( 'wp_block', array( 'labels' => array( 'name' => _x( 'Patterns', 'post type general name' ), 'singular_name' => _x( 'Pattern', 'post type singular name' ), 'add_new' => __( 'Add New Pattern' ), 'add_new_item' => __( 'Add New Pattern' ), 'new_item' => __( 'New Pattern' ), 'edit_item' => __( 'Edit Block Pattern' ), 'view_item' => __( 'View Pattern' ), 'view_items' => __( 'View Patterns' ), 'all_items' => __( 'All Patterns' ), 'search_items' => __( 'Search Patterns' ), 'not_found' => __( 'No patterns found.' ), 'not_found_in_trash' => __( 'No patterns found in Trash.' ), 'filter_items_list' => __( 'Filter patterns list' ), 'items_list_navigation' => __( 'Patterns list navigation' ), 'items_list' => __( 'Patterns list' ), 'item_published' => __( 'Pattern published.' ), 'item_published_privately' => __( 'Pattern published privately.' ), 'item_reverted_to_draft' => __( 'Pattern reverted to draft.' ), 'item_scheduled' => __( 'Pattern scheduled.' ), 'item_updated' => __( 'Pattern updated.' ), ), 'public' => false, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ 'show_ui' => true, 'show_in_menu' => false, 'rewrite' => false, 'show_in_rest' => true, 'rest_base' => 'blocks', 'rest_controller_class' => 'WP_REST_Blocks_Controller', 'capability_type' => 'block', 'capabilities' => array( // You need to be able to edit posts, in order to read blocks in their raw form. 'read' => 'edit_posts', // You need to be able to publish posts, in order to create blocks. 'create_posts' => 'publish_posts', 'edit_posts' => 'edit_posts', 'edit_published_posts' => 'edit_published_posts', 'delete_published_posts' => 'delete_published_posts', // Enables trashing draft posts as well. 'delete_posts' => 'delete_posts', 'edit_others_posts' => 'edit_others_posts', 'delete_others_posts' => 'delete_others_posts', ), 'map_meta_cap' => true, 'supports' => array( 'title', 'editor', 'revisions', 'custom-fields', ), ) ); $template_edit_link = 'site-editor.php?' . build_query( array( 'postType' => '%s', 'postId' => '%s', 'canvas' => 'edit', ) ); register_post_type( 'wp_template', array( 'labels' => array( 'name' => _x( 'Templates', 'post type general name' ), 'singular_name' => _x( 'Template', 'post type singular name' ), 'add_new' => __( 'Add New Template' ), 'add_new_item' => __( 'Add New Template' ), 'new_item' => __( 'New Template' ), 'edit_item' => __( 'Edit Template' ), 'view_item' => __( 'View Template' ), 'all_items' => __( 'Templates' ), 'search_items' => __( 'Search Templates' ), 'parent_item_colon' => __( 'Parent Template:' ), 'not_found' => __( 'No templates found.' ), 'not_found_in_trash' => __( 'No templates found in Trash.' ), 'archives' => __( 'Template archives' ), 'insert_into_item' => __( 'Insert into template' ), 'uploaded_to_this_item' => __( 'Uploaded to this template' ), 'filter_items_list' => __( 'Filter templates list' ), 'items_list_navigation' => __( 'Templates list navigation' ), 'items_list' => __( 'Templates list' ), ), 'description' => __( 'Templates to include in your theme.' ), 'public' => false, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ '_edit_link' => $template_edit_link, /* internal use only. don't use this when registering your own post type. */ 'has_archive' => false, 'show_ui' => false, 'show_in_menu' => false, 'show_in_rest' => true, 'rewrite' => false, 'rest_base' => 'templates', 'rest_controller_class' => 'WP_REST_Templates_Controller', 'autosave_rest_controller_class' => 'WP_REST_Template_Autosaves_Controller', 'revisions_rest_controller_class' => 'WP_REST_Template_Revisions_Controller', 'late_route_registration' => true, 'capability_type' => array( 'template', 'templates' ), 'capabilities' => array( 'create_posts' => 'edit_theme_options', 'delete_posts' => 'edit_theme_options', 'delete_others_posts' => 'edit_theme_options', 'delete_private_posts' => 'edit_theme_options', 'delete_published_posts' => 'edit_theme_options', 'edit_posts' => 'edit_theme_options', 'edit_others_posts' => 'edit_theme_options', 'edit_private_posts' => 'edit_theme_options', 'edit_published_posts' => 'edit_theme_options', 'publish_posts' => 'edit_theme_options', 'read' => 'edit_theme_options', 'read_private_posts' => 'edit_theme_options', ), 'map_meta_cap' => true, 'supports' => array( 'title', 'slug', 'excerpt', 'editor', 'revisions', 'author', ), ) ); register_post_type( 'wp_template_part', array( 'labels' => array( 'name' => _x( 'Template Parts', 'post type general name' ), 'singular_name' => _x( 'Template Part', 'post type singular name' ), 'add_new' => __( 'Add New Template Part' ), 'add_new_item' => __( 'Add New Template Part' ), 'new_item' => __( 'New Template Part' ), 'edit_item' => __( 'Edit Template Part' ), 'view_item' => __( 'View Template Part' ), 'all_items' => __( 'Template Parts' ), 'search_items' => __( 'Search Template Parts' ), 'parent_item_colon' => __( 'Parent Template Part:' ), 'not_found' => __( 'No template parts found.' ), 'not_found_in_trash' => __( 'No template parts found in Trash.' ), 'archives' => __( 'Template part archives' ), 'insert_into_item' => __( 'Insert into template part' ), 'uploaded_to_this_item' => __( 'Uploaded to this template part' ), 'filter_items_list' => __( 'Filter template parts list' ), 'items_list_navigation' => __( 'Template parts list navigation' ), 'items_list' => __( 'Template parts list' ), ), 'description' => __( 'Template parts to include in your templates.' ), 'public' => false, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ '_edit_link' => $template_edit_link, /* internal use only. don't use this when registering your own post type. */ 'has_archive' => false, 'show_ui' => false, 'show_in_menu' => false, 'show_in_rest' => true, 'rewrite' => false, 'rest_base' => 'template-parts', 'rest_controller_class' => 'WP_REST_Templates_Controller', 'autosave_rest_controller_class' => 'WP_REST_Template_Autosaves_Controller', 'revisions_rest_controller_class' => 'WP_REST_Template_Revisions_Controller', 'late_route_registration' => true, 'map_meta_cap' => true, 'capabilities' => array( 'create_posts' => 'edit_theme_options', 'delete_posts' => 'edit_theme_options', 'delete_others_posts' => 'edit_theme_options', 'delete_private_posts' => 'edit_theme_options', 'delete_published_posts' => 'edit_theme_options', 'edit_posts' => 'edit_theme_options', 'edit_others_posts' => 'edit_theme_options', 'edit_private_posts' => 'edit_theme_options', 'edit_published_posts' => 'edit_theme_options', 'publish_posts' => 'edit_theme_options', 'read' => 'edit_theme_options', 'read_private_posts' => 'edit_theme_options', ), 'supports' => array( 'title', 'slug', 'excerpt', 'editor', 'revisions', 'author', ), ) ); register_post_type( 'wp_global_styles', array( 'label' => _x( 'Global Styles', 'post type general name' ), 'description' => __( 'Global styles to include in themes.' ), 'public' => false, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ '_edit_link' => '/site-editor.php?canvas=edit', /* internal use only. don't use this when registering your own post type. */ 'show_ui' => false, 'show_in_rest' => false, 'rewrite' => false, 'capabilities' => array( 'read' => 'edit_theme_options', 'create_posts' => 'edit_theme_options', 'edit_posts' => 'edit_theme_options', 'edit_published_posts' => 'edit_theme_options', 'delete_published_posts' => 'edit_theme_options', 'edit_others_posts' => 'edit_theme_options', 'delete_others_posts' => 'edit_theme_options', ), 'map_meta_cap' => true, 'supports' => array( 'title', 'editor', 'revisions', ), ) ); $navigation_post_edit_link = 'site-editor.php?' . build_query( array( 'postId' => '%s', 'postType' => 'wp_navigation', 'canvas' => 'edit', ) ); register_post_type( 'wp_navigation', array( 'labels' => array( 'name' => _x( 'Navigation Menus', 'post type general name' ), 'singular_name' => _x( 'Navigation Menu', 'post type singular name' ), 'add_new' => __( 'Add New Navigation Menu' ), 'add_new_item' => __( 'Add New Navigation Menu' ), 'new_item' => __( 'New Navigation Menu' ), 'edit_item' => __( 'Edit Navigation Menu' ), 'view_item' => __( 'View Navigation Menu' ), 'all_items' => __( 'Navigation Menus' ), 'search_items' => __( 'Search Navigation Menus' ), 'parent_item_colon' => __( 'Parent Navigation Menu:' ), 'not_found' => __( 'No Navigation Menu found.' ), 'not_found_in_trash' => __( 'No Navigation Menu found in Trash.' ), 'archives' => __( 'Navigation Menu archives' ), 'insert_into_item' => __( 'Insert into Navigation Menu' ), 'uploaded_to_this_item' => __( 'Uploaded to this Navigation Menu' ), 'filter_items_list' => __( 'Filter Navigation Menu list' ), 'items_list_navigation' => __( 'Navigation Menus list navigation' ), 'items_list' => __( 'Navigation Menus list' ), ), 'description' => __( 'Navigation menus that can be inserted into your site.' ), 'public' => false, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ '_edit_link' => $navigation_post_edit_link, /* internal use only. don't use this when registering your own post type. */ 'has_archive' => false, 'show_ui' => true, 'show_in_menu' => false, 'show_in_admin_bar' => false, 'show_in_rest' => true, 'rewrite' => false, 'map_meta_cap' => true, 'capabilities' => array( 'edit_others_posts' => 'edit_theme_options', 'delete_posts' => 'edit_theme_options', 'publish_posts' => 'edit_theme_options', 'create_posts' => 'edit_theme_options', 'read_private_posts' => 'edit_theme_options', 'delete_private_posts' => 'edit_theme_options', 'delete_published_posts' => 'edit_theme_options', 'delete_others_posts' => 'edit_theme_options', 'edit_private_posts' => 'edit_theme_options', 'edit_published_posts' => 'edit_theme_options', 'edit_posts' => 'edit_theme_options', ), 'rest_base' => 'navigation', 'rest_controller_class' => 'WP_REST_Posts_Controller', 'supports' => array( 'title', 'editor', 'revisions', ), ) ); register_post_status( 'publish', array( 'label' => _x( 'Published', 'post status' ), 'public' => true, '_builtin' => true, /* internal use only. */ /* translators: %s: Number of published posts. */ 'label_count' => _n_noop( 'Published (%s)', 'Published (%s)' ), ) ); register_post_status( 'future', array( 'label' => _x( 'Scheduled', 'post status' ), 'protected' => true, '_builtin' => true, /* internal use only. */ /* translators: %s: Number of scheduled posts. */ 'label_count' => _n_noop( 'Scheduled (%s)', 'Scheduled (%s)' ), ) ); register_post_status( 'draft', array( 'label' => _x( 'Draft', 'post status' ), 'protected' => true, '_builtin' => true, /* internal use only. */ /* translators: %s: Number of draft posts. */ 'label_count' => _n_noop( 'Draft (%s)', 'Drafts (%s)' ), 'date_floating' => true, ) ); register_post_status( 'pending', array( 'label' => _x( 'Pending', 'post status' ), 'protected' => true, '_builtin' => true, /* internal use only. */ /* translators: %s: Number of pending posts. */ 'label_count' => _n_noop( 'Pending (%s)', 'Pending (%s)' ), 'date_floating' => true, ) ); register_post_status( 'private', array( 'label' => _x( 'Private', 'post status' ), 'private' => true, '_builtin' => true, /* internal use only. */ /* translators: %s: Number of private posts. */ 'label_count' => _n_noop( 'Private (%s)', 'Private (%s)' ), ) ); register_post_status( 'trash', array( 'label' => _x( 'Trash', 'post status' ), 'internal' => true, '_builtin' => true, /* internal use only. */ /* translators: %s: Number of trashed posts. */ 'label_count' => _n_noop( 'Trash (%s)', 'Trash (%s)' ), 'show_in_admin_status_list' => true, ) ); register_post_status( 'auto-draft', array( 'label' => 'auto-draft', 'internal' => true, '_builtin' => true, /* internal use only. */ 'date_floating' => true, ) ); register_post_status( 'inherit', array( 'label' => 'inherit', 'internal' => true, '_builtin' => true, /* internal use only. */ 'exclude_from_search' => false, ) ); register_post_status( 'request-pending', array( 'label' => _x( 'Pending', 'request status' ), 'internal' => true, '_builtin' => true, /* internal use only. */ /* translators: %s: Number of pending requests. */ 'label_count' => _n_noop( 'Pending (%s)', 'Pending (%s)' ), 'exclude_from_search' => false, ) ); register_post_status( 'request-confirmed', array( 'label' => _x( 'Confirmed', 'request status' ), 'internal' => true, '_builtin' => true, /* internal use only. */ /* translators: %s: Number of confirmed requests. */ 'label_count' => _n_noop( 'Confirmed (%s)', 'Confirmed (%s)' ), 'exclude_from_search' => false, ) ); register_post_status( 'request-failed', array( 'label' => _x( 'Failed', 'request status' ), 'internal' => true, '_builtin' => true, /* internal use only. */ /* translators: %s: Number of failed requests. */ 'label_count' => _n_noop( 'Failed (%s)', 'Failed (%s)' ), 'exclude_from_search' => false, ) ); register_post_status( 'request-completed', array( 'label' => _x( 'Completed', 'request status' ), 'internal' => true, '_builtin' => true, /* internal use only. */ /* translators: %s: Number of completed requests. */ 'label_count' => _n_noop( 'Completed (%s)', 'Completed (%s)' ), 'exclude_from_search' => false, ) ); } /** * Retrieves attached file path based on attachment ID. * * By default the path will go through the {@see 'get_attached_file'} filter, but * passing `true` to the `$unfiltered` argument will return the file path unfiltered. * * The function works by retrieving the `_wp_attached_file` post meta value. * This is a convenience function to prevent looking up the meta name and provide * a mechanism for sending the attached filename through a filter. * * @since 2.0.0 * * @param int $attachment_id Attachment ID. * @param bool $unfiltered Optional. Whether to skip the {@see 'get_attached_file'} filter. * Default false. * @return string|false The file path to where the attached file should be, false otherwise. */ function get_attached_file( $attachment_id, $unfiltered = false ) { $file = get_post_meta( $attachment_id, '_wp_attached_file', true ); // If the file is relative, prepend upload dir. if ( $file && ! str_starts_with( $file, '/' ) && ! preg_match( '|^.:\\\|', $file ) ) { $uploads = wp_get_upload_dir(); if ( false === $uploads['error'] ) { $file = $uploads['basedir'] . "/$file"; } } if ( $unfiltered ) { return $file; } /** * Filters the attached file based on the given ID. * * @since 2.1.0 * * @param string|false $file The file path to where the attached file should be, false otherwise. * @param int $attachment_id Attachment ID. */ return apply_filters( 'get_attached_file', $file, $attachment_id ); } /** * Updates attachment file path based on attachment ID. * * Used to update the file path of the attachment, which uses post meta name * '_wp_attached_file' to store the path of the attachment. * * @since 2.1.0 * * @param int $attachment_id Attachment ID. * @param string $file File path for the attachment. * @return bool True on success, false on failure. */ function update_attached_file( $attachment_id, $file ) { if ( ! get_post( $attachment_id ) ) { return false; } /** * Filters the path to the attached file to update. * * @since 2.1.0 * * @param string $file Path to the attached file to update. * @param int $attachment_id Attachment ID. */ $file = apply_filters( 'update_attached_file', $file, $attachment_id ); $file = _wp_relative_upload_path( $file ); if ( $file ) { return update_post_meta( $attachment_id, '_wp_attached_file', $file ); } else { return delete_post_meta( $attachment_id, '_wp_attached_file' ); } } /** * Returns relative path to an uploaded file. * * The path is relative to the current upload dir. * * @since 2.9.0 * @access private * * @param string $path Full path to the file. * @return string Relative path on success, unchanged path on failure. */ function _wp_relative_upload_path( $path ) { $new_path = $path; $uploads = wp_get_upload_dir(); if ( str_starts_with( $new_path, $uploads['basedir'] ) ) { $new_path = str_replace( $uploads['basedir'], '', $new_path ); $new_path = ltrim( $new_path, '/' ); } /** * Filters the relative path to an uploaded file. * * @since 2.9.0 * * @param string $new_path Relative path to the file. * @param string $path Full path to the file. */ return apply_filters( '_wp_relative_upload_path', $new_path, $path ); } /** * Retrieves all children of the post parent ID. * * Normally, without any enhancements, the children would apply to pages. In the * context of the inner workings of WordPress, pages, posts, and attachments * share the same table, so therefore the functionality could apply to any one * of them. It is then noted that while this function does not work on posts, it * does not mean that it won't work on posts. It is recommended that you know * what context you wish to retrieve the children of. * * Attachments may also be made the child of a post, so if that is an accurate * statement (which needs to be verified), it would then be possible to get * all of the attachments for a post. Attachments have since changed since * version 2.5, so this is most likely inaccurate, but serves generally as an * example of what is possible. * * The arguments listed as defaults are for this function and also of the * get_posts() function. The arguments are combined with the get_children defaults * and are then passed to the get_posts() function, which accepts additional arguments. * You can replace the defaults in this function, listed below and the additional * arguments listed in the get_posts() function. * * The 'post_parent' is the most important argument and important attention * needs to be paid to the $args parameter. If you pass either an object or an * integer (number), then just the 'post_parent' is grabbed and everything else * is lost. If you don't specify any arguments, then it is assumed that you are * in The Loop and the post parent will be grabbed for from the current post. * * The 'post_parent' argument is the ID to get the children. The 'numberposts' * is the amount of posts to retrieve that has a default of '-1', which is * used to get all of the posts. Giving a number higher than 0 will only * retrieve that amount of posts. * * The 'post_type' and 'post_status' arguments can be used to choose what * criteria of posts to retrieve. The 'post_type' can be anything, but WordPress * post types are 'post', 'pages', and 'attachments'. The 'post_status' * argument will accept any post status within the write administration panels. * * @since 2.0.0 * * @see get_posts() * @todo Check validity of description. * * @global WP_Post $post Global post object. * * @param mixed $args Optional. User defined arguments for replacing the defaults. Default empty. * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which * correspond to a WP_Post object, an associative array, or a numeric array, * respectively. Default OBJECT. * @return WP_Post[]|array[]|int[] Array of post objects, arrays, or IDs, depending on `$output`. */ function get_children( $args = '', $output = OBJECT ) { $kids = array(); if ( empty( $args ) ) { if ( isset( $GLOBALS['post'] ) ) { $args = array( 'post_parent' => (int) $GLOBALS['post']->post_parent ); } else { return $kids; } } elseif ( is_object( $args ) ) { $args = array( 'post_parent' => (int) $args->post_parent ); } elseif ( is_numeric( $args ) ) { $args = array( 'post_parent' => (int) $args ); } $defaults = array( 'numberposts' => -1, 'post_type' => 'any', 'post_status' => 'any', 'post_parent' => 0, ); $parsed_args = wp_parse_args( $args, $defaults ); $children = get_posts( $parsed_args ); if ( ! $children ) { return $kids; } if ( ! empty( $parsed_args['fields'] ) ) { return $children; } update_post_cache( $children ); foreach ( $children as $key => $child ) { $kids[ $child->ID ] = $children[ $key ]; } if ( OBJECT === $output ) { return $kids; } elseif ( ARRAY_A === $output ) { $weeuns = array(); foreach ( (array) $kids as $kid ) { $weeuns[ $kid->ID ] = get_object_vars( $kids[ $kid->ID ] ); } return $weeuns; } elseif ( ARRAY_N === $output ) { $babes = array(); foreach ( (array) $kids as $kid ) { $babes[ $kid->ID ] = array_values( get_object_vars( $kids[ $kid->ID ] ) ); } return $babes; } else { return $kids; } } /** * Gets extended entry info (). * * There should not be any space after the second dash and before the word * 'more'. There can be text or space(s) after the word 'more', but won't be * referenced. * * The returned array has 'main', 'extended', and 'more_text' keys. Main has the text before * the ``. The 'extended' key has the content after the * `` comment. The 'more_text' key has the custom "Read More" text. * * @since 1.0.0 * * @param string $post Post content. * @return string[] { * Extended entry info. * * @type string $main Content before the more tag. * @type string $extended Content after the more tag. * @type string $more_text Custom read more text, or empty string. * } */ function get_extended( $post ) { // Match the new style more links. if ( preg_match( '//', $post, $matches ) ) { list($main, $extended) = explode( $matches[0], $post, 2 ); $more_text = $matches[1]; } else { $main = $post; $extended = ''; $more_text = ''; } // Leading and trailing whitespace. $main = preg_replace( '/^[\s]*(.*)[\s]*$/', '\\1', $main ); $extended = preg_replace( '/^[\s]*(.*)[\s]*$/', '\\1', $extended ); $more_text = preg_replace( '/^[\s]*(.*)[\s]*$/', '\\1', $more_text ); return array( 'main' => $main, 'extended' => $extended, 'more_text' => $more_text, ); } /** * Retrieves post data given a post ID or post object. * * See sanitize_post() for optional $filter values. Also, the parameter * `$post`, must be given as a variable, since it is passed by reference. * * @since 1.5.1 * * @global WP_Post $post Global post object. * * @param int|WP_Post|null $post Optional. Post ID or post object. `null`, `false`, `0` and other PHP falsey values * return the current global post inside the loop. A numerically valid post ID that * points to a non-existent post returns `null`. Defaults to global $post. * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which * correspond to a WP_Post object, an associative array, or a numeric array, * respectively. Default OBJECT. * @param string $filter Optional. Type of filter to apply. Accepts 'raw', 'edit', 'db', * or 'display'. Default 'raw'. * @return WP_Post|array|null Type corresponding to $output on success or null on failure. * When $output is OBJECT, a `WP_Post` instance is returned. */ function get_post( $post = null, $output = OBJECT, $filter = 'raw' ) { if ( empty( $post ) && isset( $GLOBALS['post'] ) ) { $post = $GLOBALS['post']; } if ( $post instanceof WP_Post ) { $_post = $post; } elseif ( is_object( $post ) ) { if ( empty( $post->filter ) ) { $_post = sanitize_post( $post, 'raw' ); $_post = new WP_Post( $_post ); } elseif ( 'raw' === $post->filter ) { $_post = new WP_Post( $post ); } else { $_post = WP_Post::get_instance( $post->ID ); } } else { $_post = WP_Post::get_instance( $post ); } if ( ! $_post ) { return null; } $_post = $_post->filter( $filter ); if ( ARRAY_A === $output ) { return $_post->to_array(); } elseif ( ARRAY_N === $output ) { return array_values( $_post->to_array() ); } return $_post; } /** * Retrieves the IDs of the ancestors of a post. * * @since 2.5.0 * * @param int|WP_Post $post Post ID or post object. * @return int[] Array of ancestor IDs or empty array if there are none. */ function get_post_ancestors( $post ) { $post = get_post( $post ); if ( ! $post || empty( $post->post_parent ) || $post->post_parent == $post->ID ) { return array(); } $ancestors = array(); $id = $post->post_parent; $ancestors[] = $id; while ( $ancestor = get_post( $id ) ) { // Loop detection: If the ancestor has been seen before, break. if ( empty( $ancestor->post_parent ) || ( $ancestor->post_parent == $post->ID ) || in_array( $ancestor->post_parent, $ancestors, true ) ) { break; } $id = $ancestor->post_parent; $ancestors[] = $id; } return $ancestors; } /** * Retrieves data from a post field based on Post ID. * * Examples of the post field will be, 'post_type', 'post_status', 'post_content', * etc and based off of the post object property or key names. * * The context values are based off of the taxonomy filter functions and * supported values are found within those functions. * * @since 2.3.0 * @since 4.5.0 The `$post` parameter was made optional. * * @see sanitize_post_field() * * @param string $field Post field name. * @param int|WP_Post $post Optional. Post ID or post object. Defaults to global $post. * @param string $context Optional. How to filter the field. Accepts 'raw', 'edit', 'db', * or 'display'. Default 'display'. * @return string The value of the post field on success, empty string on failure. */ function get_post_field( $field, $post = null, $context = 'display' ) { $post = get_post( $post ); if ( ! $post ) { return ''; } if ( ! isset( $post->$field ) ) { return ''; } return sanitize_post_field( $field, $post->$field, $post->ID, $context ); } /** * Retrieves the mime type of an attachment based on the ID. * * This function can be used with any post type, but it makes more sense with * attachments. * * @since 2.0.0 * * @param int|WP_Post $post Optional. Post ID or post object. Defaults to global $post. * @return string|false The mime type on success, false on failure. */ function get_post_mime_type( $post = null ) { $post = get_post( $post ); if ( is_object( $post ) ) { return $post->post_mime_type; } return false; } /** * Retrieves the post status based on the post ID. * * If the post ID is of an attachment, then the parent post status will be given * instead. * * @since 2.0.0 * * @param int|WP_Post $post Optional. Post ID or post object. Defaults to global $post. * @return string|false Post status on success, false on failure. */ function get_post_status( $post = null ) { $post = get_post( $post ); if ( ! is_object( $post ) ) { return false; } $post_status = $post->post_status; if ( 'attachment' === $post->post_type && 'inherit' === $post_status ) { if ( 0 === $post->post_parent || ! get_post( $post->post_parent ) || $post->ID === $post->post_parent ) { // Unattached attachments with inherit status are assumed to be published. $post_status = 'publish'; } elseif ( 'trash' === get_post_status( $post->post_parent ) ) { // Get parent status prior to trashing. $post_status = get_post_meta( $post->post_parent, '_wp_trash_meta_status', true ); if ( ! $post_status ) { // Assume publish as above. $post_status = 'publish'; } } else { $post_status = get_post_status( $post->post_parent ); } } elseif ( 'attachment' === $post->post_type && ! in_array( $post_status, array( 'private', 'trash', 'auto-draft' ), true ) ) { /* * Ensure uninherited attachments have a permitted status either 'private', 'trash', 'auto-draft'. * This is to match the logic in wp_insert_post(). * * Note: 'inherit' is excluded from this check as it is resolved to the parent post's * status in the logic block above. */ $post_status = 'publish'; } /** * Filters the post status. * * @since 4.4.0 * @since 5.7.0 The attachment post type is now passed through this filter. * * @param string $post_status The post status. * @param WP_Post $post The post object. */ return apply_filters( 'get_post_status', $post_status, $post ); } /** * Retrieves all of the WordPress supported post statuses. * * Posts have a limited set of valid status values, this provides the * post_status values and descriptions. * * @since 2.5.0 * * @return string[] Array of post status labels keyed by their status. */ function get_post_statuses() { $status = array( 'draft' => __( 'Draft' ), 'pending' => __( 'Pending Review' ), 'private' => __( 'Private' ), 'publish' => __( 'Published' ), ); return $status; } /** * Retrieves all of the WordPress support page statuses. * * Pages have a limited set of valid status values, this provides the * post_status values and descriptions. * * @since 2.5.0 * * @return string[] Array of page status labels keyed by their status. */ function get_page_statuses() { $status = array( 'draft' => __( 'Draft' ), 'private' => __( 'Private' ), 'publish' => __( 'Published' ), ); return $status; } /** * Returns statuses for privacy requests. * * @since 4.9.6 * @access private * * @return string[] Array of privacy request status labels keyed by their status. */ function _wp_privacy_statuses() { return array( 'request-pending' => _x( 'Pending', 'request status' ), // Pending confirmation from user. 'request-confirmed' => _x( 'Confirmed', 'request status' ), // User has confirmed the action. 'request-failed' => _x( 'Failed', 'request status' ), // User failed to confirm the action. 'request-completed' => _x( 'Completed', 'request status' ), // Admin has handled the request. ); } /** * Registers a post status. Do not use before init. * * A simple function for creating or modifying a post status based on the * parameters given. The function will accept an array (second optional * parameter), along with a string for the post status name. * * Arguments prefixed with an _underscore shouldn't be used by plugins and themes. * * @since 3.0.0 * * @global stdClass[] $wp_post_statuses Inserts new post status object into the list * * @param string $post_status Name of the post status. * @param array|string $args { * Optional. Array or string of post status arguments. * * @type bool|string $label A descriptive name for the post status marked * for translation. Defaults to value of $post_status. * @type array|false $label_count Nooped plural text from _n_noop() to provide the singular * and plural forms of the label for counts. Default false * which means the `$label` argument will be used for both * the singular and plural forms of this label. * @type bool $exclude_from_search Whether to exclude posts with this post status * from search results. Default is value of $internal. * @type bool $_builtin Whether the status is built-in. Core-use only. * Default false. * @type bool $public Whether posts of this status should be shown * in the front end of the site. Default false. * @type bool $internal Whether the status is for internal use only. * Default false. * @type bool $protected Whether posts with this status should be protected. * Default false. * @type bool $private Whether posts with this status should be private. * Default false. * @type bool $publicly_queryable Whether posts with this status should be publicly- * queryable. Default is value of $public. * @type bool $show_in_admin_all_list Whether to include posts in the edit listing for * their post type. Default is the opposite value * of $internal. * @type bool $show_in_admin_status_list Show in the list of statuses with post counts at * the top of the edit listings, * e.g. All (12) | Published (9) | My Custom Status (2) * Default is the opposite value of $internal. * @type bool $date_floating Whether the post has a floating creation date. * Default to false. * } * @return object */ function register_post_status( $post_status, $args = array() ) { global $wp_post_statuses; if ( ! is_array( $wp_post_statuses ) ) { $wp_post_statuses = array(); } // Args prefixed with an underscore are reserved for internal use. $defaults = array( 'label' => false, 'label_count' => false, 'exclude_from_search' => null, '_builtin' => false, 'public' => null, 'internal' => null, 'protected' => null, 'private' => null, 'publicly_queryable' => null, 'show_in_admin_status_list' => null, 'show_in_admin_all_list' => null, 'date_floating' => null, ); $args = wp_parse_args( $args, $defaults ); $args = (object) $args; $post_status = sanitize_key( $post_status ); $args->name = $post_status; // Set various defaults. if ( null === $args->public && null === $args->internal && null === $args->protected && null === $args->private ) { $args->internal = true; } if ( null === $args->public ) { $args->public = false; } if ( null === $args->private ) { $args->private = false; } if ( null === $args->protected ) { $args->protected = false; } if ( null === $args->internal ) { $args->internal = false; } if ( null === $args->publicly_queryable ) { $args->publicly_queryable = $args->public; } if ( null === $args->exclude_from_search ) { $args->exclude_from_search = $args->internal; } if ( null === $args->show_in_admin_all_list ) { $args->show_in_admin_all_list = ! $args->internal; } if ( null === $args->show_in_admin_status_list ) { $args->show_in_admin_status_list = ! $args->internal; } if ( null === $args->date_floating ) { $args->date_floating = false; } if ( false === $args->label ) { $args->label = $post_status; } if ( false === $args->label_count ) { // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralSingular,WordPress.WP.I18n.NonSingularStringLiteralPlural $args->label_count = _n_noop( $args->label, $args->label ); } $wp_post_statuses[ $post_status ] = $args; return $args; } /** * Retrieves a post status object by name. * * @since 3.0.0 * * @global stdClass[] $wp_post_statuses List of post statuses. * * @see register_post_status() * * @param string $post_status The name of a registered post status. * @return stdClass|null A post status object. */ function get_post_status_object( $post_status ) { global $wp_post_statuses; if ( empty( $wp_post_statuses[ $post_status ] ) ) { return null; } return $wp_post_statuses[ $post_status ]; } /** * Gets a list of post statuses. * * @since 3.0.0 * * @global stdClass[] $wp_post_statuses List of post statuses. * * @see register_post_status() * * @param array|string $args Optional. Array or string of post status arguments to compare against * properties of the global `$wp_post_statuses objects`. Default empty array. * @param string $output Optional. The type of output to return, either 'names' or 'objects'. Default 'names'. * @param string $operator Optional. The logical operation to perform. 'or' means only one element * from the array needs to match; 'and' means all elements must match. * Default 'and'. * @return string[]|stdClass[] A list of post status names or objects. */ function get_post_stati( $args = array(), $output = 'names', $operator = 'and' ) { global $wp_post_statuses; $field = ( 'names' === $output ) ? 'name' : false; return wp_filter_object_list( $wp_post_statuses, $args, $operator, $field ); } /** * Determines whether the post type is hierarchical. * * A false return value might also mean that the post type does not exist. * * @since 3.0.0 * * @see get_post_type_object() * * @param string $post_type Post type name * @return bool Whether post type is hierarchical. */ function is_post_type_hierarchical( $post_type ) { if ( ! post_type_exists( $post_type ) ) { return false; } $post_type = get_post_type_object( $post_type ); return $post_type->hierarchical; } /** * Determines whether a post type is registered. * * For more information on this and similar theme functions, check out * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ * Conditional Tags} article in the Theme Developer Handbook. * * @since 3.0.0 * * @see get_post_type_object() * * @param string $post_type Post type name. * @return bool Whether post type is registered. */ function post_type_exists( $post_type ) { return (bool) get_post_type_object( $post_type ); } /** * Retrieves the post type of the current post or of a given post. * * @since 2.1.0 * * @param int|WP_Post|null $post Optional. Post ID or post object. Default is global $post. * @return string|false Post type on success, false on failure. */ function get_post_type( $post = null ) { $post = get_post( $post ); if ( $post ) { return $post->post_type; } return false; } /** * Retrieves a post type object by name. * * @since 3.0.0 * @since 4.6.0 Object returned is now an instance of `WP_Post_Type`. * * @global array $wp_post_types List of post types. * * @see register_post_type() * * @param string $post_type The name of a registered post type. * @return WP_Post_Type|null WP_Post_Type object if it exists, null otherwise. */ function get_post_type_object( $post_type ) { global $wp_post_types; if ( ! is_scalar( $post_type ) || empty( $wp_post_types[ $post_type ] ) ) { return null; } return $wp_post_types[ $post_type ]; } /** * Gets a list of all registered post type objects. * * @since 2.9.0 * * @global array $wp_post_types List of post types. * * @see register_post_type() for accepted arguments. * * @param array|string $args Optional. An array of key => value arguments to match against * the post type objects. Default empty array. * @param string $output Optional. The type of output to return. Accepts post type 'names' * or 'objects'. Default 'names'. * @param string $operator Optional. The logical operation to perform. 'or' means only one * element from the array needs to match; 'and' means all elements * must match; 'not' means no elements may match. Default 'and'. * @return string[]|WP_Post_Type[] An array of post type names or objects. */ function get_post_types( $args = array(), $output = 'names', $operator = 'and' ) { global $wp_post_types; $field = ( 'names' === $output ) ? 'name' : false; return wp_filter_object_list( $wp_post_types, $args, $operator, $field ); } /** * Registers a post type. * * Note: Post type registrations should not be hooked before the * {@see 'init'} action. Also, any taxonomy connections should be * registered via the `$taxonomies` argument to ensure consistency * when hooks such as {@see 'parse_query'} or {@see 'pre_get_posts'} * are used. * * Post types can support any number of built-in core features such * as meta boxes, custom fields, post thumbnails, post statuses, * comments, and more. See the `$supports` argument for a complete * list of supported features. * * @since 2.9.0 * @since 3.0.0 The `show_ui` argument is now enforced on the new post screen. * @since 4.4.0 The `show_ui` argument is now enforced on the post type listing * screen and post editing screen. * @since 4.6.0 Post type object returned is now an instance of `WP_Post_Type`. * @since 4.7.0 Introduced `show_in_rest`, `rest_base` and `rest_controller_class` * arguments to register the post type in REST API. * @since 5.0.0 The `template` and `template_lock` arguments were added. * @since 5.3.0 The `supports` argument will now accept an array of arguments for a feature. * @since 5.9.0 The `rest_namespace` argument was added. * * @global array $wp_post_types List of post types. * * @param string $post_type Post type key. Must not exceed 20 characters and may only contain * lowercase alphanumeric characters, dashes, and underscores. See sanitize_key(). * @param array|string $args { * Array or string of arguments for registering a post type. * * @type string $label Name of the post type shown in the menu. Usually plural. * Default is value of $labels['name']. * @type string[] $labels An array of labels for this post type. If not set, post * labels are inherited for non-hierarchical types and page * labels for hierarchical ones. See get_post_type_labels() for a full * list of supported labels. * @type string $description A short descriptive summary of what the post type is. * Default empty. * @type bool $public Whether a post type is intended for use publicly either via * the admin interface or by front-end users. While the default * settings of $exclude_from_search, $publicly_queryable, $show_ui, * and $show_in_nav_menus are inherited from $public, each does not * rely on this relationship and controls a very specific intention. * Default false. * @type bool $hierarchical Whether the post type is hierarchical (e.g. page). Default false. * @type bool $exclude_from_search Whether to exclude posts with this post type from front end search * results. Default is the opposite value of $public. * @type bool $publicly_queryable Whether queries can be performed on the front end for the post type * as part of parse_request(). Endpoints would include: * * ?post_type={post_type_key} * * ?{post_type_key}={single_post_slug} * * ?{post_type_query_var}={single_post_slug} * If not set, the default is inherited from $public. * @type bool $show_ui Whether to generate and allow a UI for managing this post type in the * admin. Default is value of $public. * @type bool|string $show_in_menu Where to show the post type in the admin menu. To work, $show_ui * must be true. If true, the post type is shown in its own top level * menu. If false, no menu is shown. If a string of an existing top * level menu ('tools.php' or 'edit.php?post_type=page', for example), the * post type will be placed as a sub-menu of that. * Default is value of $show_ui. * @type bool $show_in_nav_menus Makes this post type available for selection in navigation menus. * Default is value of $public. * @type bool $show_in_admin_bar Makes this post type available via the admin bar. Default is value * of $show_in_menu. * @type bool $show_in_rest Whether to include the post type in the REST API. Set this to true * for the post type to be available in the block editor. * @type string $rest_base To change the base URL of REST API route. Default is $post_type. * @type string $rest_namespace To change the namespace URL of REST API route. Default is wp/v2. * @type string $rest_controller_class REST API controller class name. Default is 'WP_REST_Posts_Controller'. * @type string|bool $autosave_rest_controller_class REST API controller class name. Default is 'WP_REST_Autosaves_Controller'. * @type string|bool $revisions_rest_controller_class REST API controller class name. Default is 'WP_REST_Revisions_Controller'. * @type bool $late_route_registration A flag to direct the REST API controllers for autosave / revisions should be registered before/after the post type controller. * @type int $menu_position The position in the menu order the post type should appear. To work, * $show_in_menu must be true. Default null (at the bottom). * @type string $menu_icon The URL to the icon to be used for this menu. Pass a base64-encoded * SVG using a data URI, which will be colored to match the color scheme * -- this should begin with 'data:image/svg+xml;base64,'. Pass the name * of a Dashicons helper class to use a font icon, e.g. * 'dashicons-chart-pie'. Pass 'none' to leave div.wp-menu-image empty * so an icon can be added via CSS. Defaults to use the posts icon. * @type string|array $capability_type The string to use to build the read, edit, and delete capabilities. * May be passed as an array to allow for alternative plurals when using * this argument as a base to construct the capabilities, e.g. * array('story', 'stories'). Default 'post'. * @type string[] $capabilities Array of capabilities for this post type. $capability_type is used * as a base to construct capabilities by default. * See get_post_type_capabilities(). * @type bool $map_meta_cap Whether to use the internal default meta capability handling. * Default false. * @type array $supports Core feature(s) the post type supports. Serves as an alias for calling * add_post_type_support() directly. Core features include 'title', * 'editor', 'comments', 'revisions', 'trackbacks', 'author', 'excerpt', * 'page-attributes', 'thumbnail', 'custom-fields', and 'post-formats'. * Additionally, the 'revisions' feature dictates whether the post type * will store revisions, and the 'comments' feature dictates whether the * comments count will show on the edit screen. A feature can also be * specified as an array of arguments to provide additional information * about supporting that feature. * Example: `array( 'my_feature', array( 'field' => 'value' ) )`. * Default is an array containing 'title' and 'editor'. * @type callable $register_meta_box_cb Provide a callback function that sets up the meta boxes for the * edit form. Do remove_meta_box() and add_meta_box() calls in the * callback. Default null. * @type string[] $taxonomies An array of taxonomy identifiers that will be registered for the * post type. Taxonomies can be registered later with register_taxonomy() * or register_taxonomy_for_object_type(). * Default empty array. * @type bool|string $has_archive Whether there should be post type archives, or if a string, the * archive slug to use. Will generate the proper rewrite rules if * $rewrite is enabled. Default false. * @type bool|array $rewrite { * Triggers the handling of rewrites for this post type. To prevent rewrite, set to false. * Defaults to true, using $post_type as slug. To specify rewrite rules, an array can be * passed with any of these keys: * * @type string $slug Customize the permastruct slug. Defaults to $post_type key. * @type bool $with_front Whether the permastruct should be prepended with WP_Rewrite::$front. * Default true. * @type bool $feeds Whether the feed permastruct should be built for this post type. * Default is value of $has_archive. * @type bool $pages Whether the permastruct should provide for pagination. Default true. * @type int $ep_mask Endpoint mask to assign. If not specified and permalink_epmask is set, * inherits from $permalink_epmask. If not specified and permalink_epmask * is not set, defaults to EP_PERMALINK. * } * @type string|bool $query_var Sets the query_var key for this post type. Defaults to $post_type * key. If false, a post type cannot be loaded at * ?{query_var}={post_slug}. If specified as a string, the query * ?{query_var_string}={post_slug} will be valid. * @type bool $can_export Whether to allow this post type to be exported. Default true. * @type bool $delete_with_user Whether to delete posts of this type when deleting a user. * * If true, posts of this type belonging to the user will be moved * to Trash when the user is deleted. * * If false, posts of this type belonging to the user will *not* * be trashed or deleted. * * If not set (the default), posts are trashed if post type supports * the 'author' feature. Otherwise posts are not trashed or deleted. * Default null. * @type array $template Array of blocks to use as the default initial state for an editor * session. Each item should be an array containing block name and * optional attributes. Default empty array. * @type string|false $template_lock Whether the block template should be locked if $template is set. * * If set to 'all', the user is unable to insert new blocks, * move existing blocks and delete blocks. * * If set to 'insert', the user is able to move existing blocks * but is unable to insert new blocks and delete blocks. * Default false. * @type bool $_builtin FOR INTERNAL USE ONLY! True if this post type is a native or * "built-in" post_type. Default false. * @type string $_edit_link FOR INTERNAL USE ONLY! URL segment to use for edit link of * this post type. Default 'post.php?post=%d'. * } * @return WP_Post_Type|WP_Error The registered post type object on success, * WP_Error object on failure. */ function register_post_type( $post_type, $args = array() ) { global $wp_post_types; if ( ! is_array( $wp_post_types ) ) { $wp_post_types = array(); } // Sanitize post type name. $post_type = sanitize_key( $post_type ); if ( empty( $post_type ) || strlen( $post_type ) > 20 ) { _doing_it_wrong( __FUNCTION__, __( 'Post type names must be between 1 and 20 characters in length.' ), '4.2.0' ); return new WP_Error( 'post_type_length_invalid', __( 'Post type names must be between 1 and 20 characters in length.' ) ); } $post_type_object = new WP_Post_Type( $post_type, $args ); $post_type_object->add_supports(); $post_type_object->add_rewrite_rules(); $post_type_object->register_meta_boxes(); $wp_post_types[ $post_type ] = $post_type_object; $post_type_object->add_hooks(); $post_type_object->register_taxonomies(); /** * Fires after a post type is registered. * * @since 3.3.0 * @since 4.6.0 Converted the `$post_type` parameter to accept a `WP_Post_Type` object. * * @param string $post_type Post type. * @param WP_Post_Type $post_type_object Arguments used to register the post type. */ do_action( 'registered_post_type', $post_type, $post_type_object ); /** * Fires after a specific post type is registered. * * The dynamic portion of the filter name, `$post_type`, refers to the post type key. * * Possible hook names include: * * - `registered_post_type_post` * - `registered_post_type_page` * * @since 6.0.0 * * @param string $post_type Post type. * @param WP_Post_Type $post_type_object Arguments used to register the post type. */ do_action( "registered_post_type_{$post_type}", $post_type, $post_type_object ); return $post_type_object; } /** * Unregisters a post type. * * Cannot be used to unregister built-in post types. * * @since 4.5.0 * * @global array $wp_post_types List of post types. * * @param string $post_type Post type to unregister. * @return true|WP_Error True on success, WP_Error on failure or if the post type doesn't exist. */ function unregister_post_type( $post_type ) { global $wp_post_types; if ( ! post_type_exists( $post_type ) ) { return new WP_Error( 'invalid_post_type', __( 'Invalid post type.' ) ); } $post_type_object = get_post_type_object( $post_type ); // Do not allow unregistering internal post types. if ( $post_type_object->_builtin ) { return new WP_Error( 'invalid_post_type', __( 'Unregistering a built-in post type is not allowed' ) ); } $post_type_object->remove_supports(); $post_type_object->remove_rewrite_rules(); $post_type_object->unregister_meta_boxes(); $post_type_object->remove_hooks(); $post_type_object->unregister_taxonomies(); unset( $wp_post_types[ $post_type ] ); /** * Fires after a post type was unregistered. * * @since 4.5.0 * * @param string $post_type Post type key. */ do_action( 'unregistered_post_type', $post_type ); return true; } /** * Builds an object with all post type capabilities out of a post type object * * Post type capabilities use the 'capability_type' argument as a base, if the * capability is not set in the 'capabilities' argument array or if the * 'capabilities' argument is not supplied. * * The capability_type argument can optionally be registered as an array, with * the first value being singular and the second plural, e.g. array('story, 'stories') * Otherwise, an 's' will be added to the value for the plural form. After * registration, capability_type will always be a string of the singular value. * * By default, eight keys are accepted as part of the capabilities array: * * - edit_post, read_post, and delete_post are meta capabilities, which are then * generally mapped to corresponding primitive capabilities depending on the * context, which would be the post being edited/read/deleted and the user or * role being checked. Thus these capabilities would generally not be granted * directly to users or roles. * * - edit_posts - Controls whether objects of this post type can be edited. * - edit_others_posts - Controls whether objects of this type owned by other users * can be edited. If the post type does not support an author, then this will * behave like edit_posts. * - delete_posts - Controls whether objects of this post type can be deleted. * - publish_posts - Controls publishing objects of this post type. * - read_private_posts - Controls whether private objects can be read. * * These five primitive capabilities are checked in core in various locations. * There are also six other primitive capabilities which are not referenced * directly in core, except in map_meta_cap(), which takes the three aforementioned * meta capabilities and translates them into one or more primitive capabilities * that must then be checked against the user or role, depending on the context. * * - read - Controls whether objects of this post type can be read. * - delete_private_posts - Controls whether private objects can be deleted. * - delete_published_posts - Controls whether published objects can be deleted. * - delete_others_posts - Controls whether objects owned by other users can be * can be deleted. If the post type does not support an author, then this will * behave like delete_posts. * - edit_private_posts - Controls whether private objects can be edited. * - edit_published_posts - Controls whether published objects can be edited. * * These additional capabilities are only used in map_meta_cap(). Thus, they are * only assigned by default if the post type is registered with the 'map_meta_cap' * argument set to true (default is false). * * @since 3.0.0 * @since 5.4.0 'delete_posts' is included in default capabilities. * * @see register_post_type() * @see map_meta_cap() * * @param object $args Post type registration arguments. * @return object Object with all the capabilities as member variables. */ function get_post_type_capabilities( $args ) { if ( ! is_array( $args->capability_type ) ) { $args->capability_type = array( $args->capability_type, $args->capability_type . 's' ); } // Singular base for meta capabilities, plural base for primitive capabilities. list( $singular_base, $plural_base ) = $args->capability_type; $default_capabilities = array( // Meta capabilities. 'edit_post' => 'edit_' . $singular_base, 'read_post' => 'read_' . $singular_base, 'delete_post' => 'delete_' . $singular_base, // Primitive capabilities used outside of map_meta_cap(): 'edit_posts' => 'edit_' . $plural_base, 'edit_others_posts' => 'edit_others_' . $plural_base, 'delete_posts' => 'delete_' . $plural_base, 'publish_posts' => 'publish_' . $plural_base, 'read_private_posts' => 'read_private_' . $plural_base, ); // Primitive capabilities used within map_meta_cap(): if ( $args->map_meta_cap ) { $default_capabilities_for_mapping = array( 'read' => 'read', 'delete_private_posts' => 'delete_private_' . $plural_base, 'delete_published_posts' => 'delete_published_' . $plural_base, 'delete_others_posts' => 'delete_others_' . $plural_base, 'edit_private_posts' => 'edit_private_' . $plural_base, 'edit_published_posts' => 'edit_published_' . $plural_base, ); $default_capabilities = array_merge( $default_capabilities, $default_capabilities_for_mapping ); } $capabilities = array_merge( $default_capabilities, $args->capabilities ); // Post creation capability simply maps to edit_posts by default: if ( ! isset( $capabilities['create_posts'] ) ) { $capabilities['create_posts'] = $capabilities['edit_posts']; } // Remember meta capabilities for future reference. if ( $args->map_meta_cap ) { _post_type_meta_capabilities( $capabilities ); } return (object) $capabilities; } /** * Stores or returns a list of post type meta caps for map_meta_cap(). * * @since 3.1.0 * @access private * * @global array $post_type_meta_caps Used to store meta capabilities. * * @param string[] $capabilities Post type meta capabilities. */ function _post_type_meta_capabilities( $capabilities = null ) { global $post_type_meta_caps; foreach ( $capabilities as $core => $custom ) { if ( in_array( $core, array( 'read_post', 'delete_post', 'edit_post' ), true ) ) { $post_type_meta_caps[ $custom ] = $core; } } } /** * Builds an object with all post type labels out of a post type object. * * Accepted keys of the label array in the post type object: * * - `name` - General name for the post type, usually plural. The same and overridden * by `$post_type_object->label`. Default is 'Posts' / 'Pages'. * - `singular_name` - Name for one object of this post type. Default is 'Post' / 'Page'. * - `add_new` - Label for adding a new item. Default is 'Add New Post' / 'Add New Page'. * - `add_new_item` - Label for adding a new singular item. Default is 'Add New Post' / 'Add New Page'. * - `edit_item` - Label for editing a singular item. Default is 'Edit Post' / 'Edit Page'. * - `new_item` - Label for the new item page title. Default is 'New Post' / 'New Page'. * - `view_item` - Label for viewing a singular item. Default is 'View Post' / 'View Page'. * - `view_items` - Label for viewing post type archives. Default is 'View Posts' / 'View Pages'. * - `search_items` - Label for searching plural items. Default is 'Search Posts' / 'Search Pages'. * - `not_found` - Label used when no items are found. Default is 'No posts found' / 'No pages found'. * - `not_found_in_trash` - Label used when no items are in the Trash. Default is 'No posts found in Trash' / * 'No pages found in Trash'. * - `parent_item_colon` - Label used to prefix parents of hierarchical items. Not used on non-hierarchical * post types. Default is 'Parent Page:'. * - `all_items` - Label to signify all items in a submenu link. Default is 'All Posts' / 'All Pages'. * - `archives` - Label for archives in nav menus. Default is 'Post Archives' / 'Page Archives'. * - `attributes` - Label for the attributes meta box. Default is 'Post Attributes' / 'Page Attributes'. * - `insert_into_item` - Label for the media frame button. Default is 'Insert into post' / 'Insert into page'. * - `uploaded_to_this_item` - Label for the media frame filter. Default is 'Uploaded to this post' / * 'Uploaded to this page'. * - `featured_image` - Label for the featured image meta box title. Default is 'Featured image'. * - `set_featured_image` - Label for setting the featured image. Default is 'Set featured image'. * - `remove_featured_image` - Label for removing the featured image. Default is 'Remove featured image'. * - `use_featured_image` - Label in the media frame for using a featured image. Default is 'Use as featured image'. * - `menu_name` - Label for the menu name. Default is the same as `name`. * - `filter_items_list` - Label for the table views hidden heading. Default is 'Filter posts list' / * 'Filter pages list'. * - `filter_by_date` - Label for the date filter in list tables. Default is 'Filter by date'. * - `items_list_navigation` - Label for the table pagination hidden heading. Default is 'Posts list navigation' / * 'Pages list navigation'. * - `items_list` - Label for the table hidden heading. Default is 'Posts list' / 'Pages list'. * - `item_published` - Label used when an item is published. Default is 'Post published.' / 'Page published.' * - `item_published_privately` - Label used when an item is published with private visibility. * Default is 'Post published privately.' / 'Page published privately.' * - `item_reverted_to_draft` - Label used when an item is switched to a draft. * Default is 'Post reverted to draft.' / 'Page reverted to draft.' * - `item_trashed` - Label used when an item is moved to Trash. Default is 'Post trashed.' / 'Page trashed.' * - `item_scheduled` - Label used when an item is scheduled for publishing. Default is 'Post scheduled.' / * 'Page scheduled.' * - `item_updated` - Label used when an item is updated. Default is 'Post updated.' / 'Page updated.' * - `item_link` - Title for a navigation link block variation. Default is 'Post Link' / 'Page Link'. * - `item_link_description` - Description for a navigation link block variation. Default is 'A link to a post.' / * 'A link to a page.' * * Above, the first default value is for non-hierarchical post types (like posts) * and the second one is for hierarchical post types (like pages). * * Note: To set labels used in post type admin notices, see the {@see 'post_updated_messages'} filter. * * @since 3.0.0 * @since 4.3.0 Added the `featured_image`, `set_featured_image`, `remove_featured_image`, * and `use_featured_image` labels. * @since 4.4.0 Added the `archives`, `insert_into_item`, `uploaded_to_this_item`, `filter_items_list`, * `items_list_navigation`, and `items_list` labels. * @since 4.6.0 Converted the `$post_type` parameter to accept a `WP_Post_Type` object. * @since 4.7.0 Added the `view_items` and `attributes` labels. * @since 5.0.0 Added the `item_published`, `item_published_privately`, `item_reverted_to_draft`, * `item_scheduled`, and `item_updated` labels. * @since 5.7.0 Added the `filter_by_date` label. * @since 5.8.0 Added the `item_link` and `item_link_description` labels. * @since 6.3.0 Added the `item_trashed` label. * @since 6.4.0 Changed default values for the `add_new` label to include the type of content. * This matches `add_new_item` and provides more context for better accessibility. * * @access private * * @param object|WP_Post_Type $post_type_object Post type object. * @return object Object with all the labels as member variables. */ function get_post_type_labels( $post_type_object ) { $nohier_vs_hier_defaults = WP_Post_Type::get_default_labels(); $nohier_vs_hier_defaults['menu_name'] = $nohier_vs_hier_defaults['name']; $labels = _get_custom_object_labels( $post_type_object, $nohier_vs_hier_defaults ); $post_type = $post_type_object->name; $default_labels = clone $labels; /** * Filters the labels of a specific post type. * * The dynamic portion of the hook name, `$post_type`, refers to * the post type slug. * * Possible hook names include: * * - `post_type_labels_post` * - `post_type_labels_page` * - `post_type_labels_attachment` * * @since 3.5.0 * * @see get_post_type_labels() for the full list of labels. * * @param object $labels Object with labels for the post type as member variables. */ $labels = apply_filters( "post_type_labels_{$post_type}", $labels ); // Ensure that the filtered labels contain all required default values. $labels = (object) array_merge( (array) $default_labels, (array) $labels ); return $labels; } /** * Builds an object with custom-something object (post type, taxonomy) labels * out of a custom-something object * * @since 3.0.0 * @access private * * @param object $data_object A custom-something object. * @param array $nohier_vs_hier_defaults Hierarchical vs non-hierarchical default labels. * @return object Object containing labels for the given custom-something object. */ function _get_custom_object_labels( $data_object, $nohier_vs_hier_defaults ) { $data_object->labels = (array) $data_object->labels; if ( isset( $data_object->label ) && empty( $data_object->labels['name'] ) ) { $data_object->labels['name'] = $data_object->label; } if ( ! isset( $data_object->labels['singular_name'] ) && isset( $data_object->labels['name'] ) ) { $data_object->labels['singular_name'] = $data_object->labels['name']; } if ( ! isset( $data_object->labels['name_admin_bar'] ) ) { $data_object->labels['name_admin_bar'] = isset( $data_object->labels['singular_name'] ) ? $data_object->labels['singular_name'] : $data_object->name; } if ( ! isset( $data_object->labels['menu_name'] ) && isset( $data_object->labels['name'] ) ) { $data_object->labels['menu_name'] = $data_object->labels['name']; } if ( ! isset( $data_object->labels['all_items'] ) && isset( $data_object->labels['menu_name'] ) ) { $data_object->labels['all_items'] = $data_object->labels['menu_name']; } if ( ! isset( $data_object->labels['archives'] ) && isset( $data_object->labels['all_items'] ) ) { $data_object->labels['archives'] = $data_object->labels['all_items']; } $defaults = array(); foreach ( $nohier_vs_hier_defaults as $key => $value ) { $defaults[ $key ] = $data_object->hierarchical ? $value[1] : $value[0]; } $labels = array_merge( $defaults, $data_object->labels ); $data_object->labels = (object) $data_object->labels; return (object) $labels; } /** * Adds submenus for post types. * * @access private * @since 3.1.0 */ function _add_post_type_submenus() { foreach ( get_post_types( array( 'show_ui' => true ) ) as $ptype ) { $ptype_obj = get_post_type_object( $ptype ); // Sub-menus only. if ( ! $ptype_obj->show_in_menu || true === $ptype_obj->show_in_menu ) { continue; } add_submenu_page( $ptype_obj->show_in_menu, $ptype_obj->labels->name, $ptype_obj->labels->all_items, $ptype_obj->cap->edit_posts, "edit.php?post_type=$ptype" ); } } /** * Registers support of certain features for a post type. * * All core features are directly associated with a functional area of the edit * screen, such as the editor or a meta box. Features include: 'title', 'editor', * 'comments', 'revisions', 'trackbacks', 'author', 'excerpt', 'page-attributes', * 'thumbnail', 'custom-fields', and 'post-formats'. * * Additionally, the 'revisions' feature dictates whether the post type will * store revisions, and the 'comments' feature dictates whether the comments * count will show on the edit screen. * * A third, optional parameter can also be passed along with a feature to provide * additional information about supporting that feature. * * Example usage: * * add_post_type_support( 'my_post_type', 'comments' ); * add_post_type_support( 'my_post_type', array( * 'author', 'excerpt', * ) ); * add_post_type_support( 'my_post_type', 'my_feature', array( * 'field' => 'value', * ) ); * * @since 3.0.0 * @since 5.3.0 Formalized the existing and already documented `...$args` parameter * by adding it to the function signature. * * @global array $_wp_post_type_features * * @param string $post_type The post type for which to add the feature. * @param string|array $feature The feature being added, accepts an array of * feature strings or a single string. * @param mixed ...$args Optional extra arguments to pass along with certain features. */ function add_post_type_support( $post_type, $feature, ...$args ) { global $_wp_post_type_features; $features = (array) $feature; foreach ( $features as $feature ) { if ( $args ) { $_wp_post_type_features[ $post_type ][ $feature ] = $args; } else { $_wp_post_type_features[ $post_type ][ $feature ] = true; } } } /** * Removes support for a feature from a post type. * * @since 3.0.0 * * @global array $_wp_post_type_features * * @param string $post_type The post type for which to remove the feature. * @param string $feature The feature being removed. */ function remove_post_type_support( $post_type, $feature ) { global $_wp_post_type_features; unset( $_wp_post_type_features[ $post_type ][ $feature ] ); } /** * Gets all the post type features * * @since 3.4.0 * * @global array $_wp_post_type_features * * @param string $post_type The post type. * @return array Post type supports list. */ function get_all_post_type_supports( $post_type ) { global $_wp_post_type_features; if ( isset( $_wp_post_type_features[ $post_type ] ) ) { return $_wp_post_type_features[ $post_type ]; } return array(); } /** * Checks a post type's support for a given feature. * * @since 3.0.0 * * @global array $_wp_post_type_features * * @param string $post_type The post type being checked. * @param string $feature The feature being checked. * @return bool Whether the post type supports the given feature. */ function post_type_supports( $post_type, $feature ) { global $_wp_post_type_features; return ( isset( $_wp_post_type_features[ $post_type ][ $feature ] ) ); } /** * Retrieves a list of post type names that support a specific feature. * * @since 4.5.0 * * @global array $_wp_post_type_features Post type features * * @param array|string $feature Single feature or an array of features the post types should support. * @param string $operator Optional. The logical operation to perform. 'or' means * only one element from the array needs to match; 'and' * means all elements must match; 'not' means no elements may * match. Default 'and'. * @return string[] A list of post type names. */ function get_post_types_by_support( $feature, $operator = 'and' ) { global $_wp_post_type_features; $features = array_fill_keys( (array) $feature, true ); return array_keys( wp_filter_object_list( $_wp_post_type_features, $features, $operator ) ); } /** * Updates the post type for the post ID. * * The page or post cache will be cleaned for the post ID. * * @since 2.5.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $post_id Optional. Post ID to change post type. Default 0. * @param string $post_type Optional. Post type. Accepts 'post' or 'page' to * name a few. Default 'post'. * @return int|false Amount of rows changed. Should be 1 for success and 0 for failure. */ function set_post_type( $post_id = 0, $post_type = 'post' ) { global $wpdb; $post_type = sanitize_post_field( 'post_type', $post_type, $post_id, 'db' ); $return = $wpdb->update( $wpdb->posts, array( 'post_type' => $post_type ), array( 'ID' => $post_id ) ); clean_post_cache( $post_id ); return $return; } /** * Determines whether a post type is considered "viewable". * * For built-in post types such as posts and pages, the 'public' value will be evaluated. * For all others, the 'publicly_queryable' value will be used. * * @since 4.4.0 * @since 4.5.0 Added the ability to pass a post type name in addition to object. * @since 4.6.0 Converted the `$post_type` parameter to accept a `WP_Post_Type` object. * @since 5.9.0 Added `is_post_type_viewable` hook to filter the result. * * @param string|WP_Post_Type $post_type Post type name or object. * @return bool Whether the post type should be considered viewable. */ function is_post_type_viewable( $post_type ) { if ( is_scalar( $post_type ) ) { $post_type = get_post_type_object( $post_type ); if ( ! $post_type ) { return false; } } if ( ! is_object( $post_type ) ) { return false; } $is_viewable = $post_type->publicly_queryable || ( $post_type->_builtin && $post_type->public ); /** * Filters whether a post type is considered "viewable". * * The returned filtered value must be a boolean type to ensure * `is_post_type_viewable()` only returns a boolean. This strictness * is by design to maintain backwards-compatibility and guard against * potential type errors in PHP 8.1+. Non-boolean values (even falsey * and truthy values) will result in the function returning false. * * @since 5.9.0 * * @param bool $is_viewable Whether the post type is "viewable" (strict type). * @param WP_Post_Type $post_type Post type object. */ return true === apply_filters( 'is_post_type_viewable', $is_viewable, $post_type ); } /** * Determines whether a post status is considered "viewable". * * For built-in post statuses such as publish and private, the 'public' value will be evaluated. * For all others, the 'publicly_queryable' value will be used. * * @since 5.7.0 * @since 5.9.0 Added `is_post_status_viewable` hook to filter the result. * * @param string|stdClass $post_status Post status name or object. * @return bool Whether the post status should be considered viewable. */ function is_post_status_viewable( $post_status ) { if ( is_scalar( $post_status ) ) { $post_status = get_post_status_object( $post_status ); if ( ! $post_status ) { return false; } } if ( ! is_object( $post_status ) || $post_status->internal || $post_status->protected ) { return false; } $is_viewable = $post_status->publicly_queryable || ( $post_status->_builtin && $post_status->public ); /** * Filters whether a post status is considered "viewable". * * The returned filtered value must be a boolean type to ensure * `is_post_status_viewable()` only returns a boolean. This strictness * is by design to maintain backwards-compatibility and guard against * potential type errors in PHP 8.1+. Non-boolean values (even falsey * and truthy values) will result in the function returning false. * * @since 5.9.0 * * @param bool $is_viewable Whether the post status is "viewable" (strict type). * @param stdClass $post_status Post status object. */ return true === apply_filters( 'is_post_status_viewable', $is_viewable, $post_status ); } /** * Determines whether a post is publicly viewable. * * Posts are considered publicly viewable if both the post status and post type * are viewable. * * @since 5.7.0 * * @param int|WP_Post|null $post Optional. Post ID or post object. Defaults to global $post. * @return bool Whether the post is publicly viewable. */ function is_post_publicly_viewable( $post = null ) { $post = get_post( $post ); if ( ! $post ) { return false; } $post_type = get_post_type( $post ); $post_status = get_post_status( $post ); return is_post_type_viewable( $post_type ) && is_post_status_viewable( $post_status ); } /** * Retrieves an array of the latest posts, or posts matching the given criteria. * * For more information on the accepted arguments, see the * {@link https://developer.wordpress.org/reference/classes/wp_query/ * WP_Query} documentation in the Developer Handbook. * * The `$ignore_sticky_posts` and `$no_found_rows` arguments are ignored by * this function and both are set to `true`. * * The defaults are as follows: * * @since 1.2.0 * * @see WP_Query * @see WP_Query::parse_query() * * @param array $args { * Optional. Arguments to retrieve posts. See WP_Query::parse_query() for all available arguments. * * @type int $numberposts Total number of posts to retrieve. Is an alias of `$posts_per_page` * in WP_Query. Accepts -1 for all. Default 5. * @type int|string $category Category ID or comma-separated list of IDs (this or any children). * Is an alias of `$cat` in WP_Query. Default 0. * @type int[] $include An array of post IDs to retrieve, sticky posts will be included. * Is an alias of `$post__in` in WP_Query. Default empty array. * @type int[] $exclude An array of post IDs not to retrieve. Default empty array. * @type bool $suppress_filters Whether to suppress filters. Default true. * } * @return WP_Post[]|int[] Array of post objects or post IDs. */ function get_posts( $args = null ) { $defaults = array( 'numberposts' => 5, 'category' => 0, 'orderby' => 'date', 'order' => 'DESC', 'include' => array(), 'exclude' => array(), 'meta_key' => '', 'meta_value' => '', 'post_type' => 'post', 'suppress_filters' => true, ); $parsed_args = wp_parse_args( $args, $defaults ); if ( empty( $parsed_args['post_status'] ) ) { $parsed_args['post_status'] = ( 'attachment' === $parsed_args['post_type'] ) ? 'inherit' : 'publish'; } if ( ! empty( $parsed_args['numberposts'] ) && empty( $parsed_args['posts_per_page'] ) ) { $parsed_args['posts_per_page'] = $parsed_args['numberposts']; } if ( ! empty( $parsed_args['category'] ) ) { $parsed_args['cat'] = $parsed_args['category']; } if ( ! empty( $parsed_args['include'] ) ) { $incposts = wp_parse_id_list( $parsed_args['include'] ); $parsed_args['posts_per_page'] = count( $incposts ); // Only the number of posts included. $parsed_args['post__in'] = $incposts; } elseif ( ! empty( $parsed_args['exclude'] ) ) { $parsed_args['post__not_in'] = wp_parse_id_list( $parsed_args['exclude'] ); } $parsed_args['ignore_sticky_posts'] = true; $parsed_args['no_found_rows'] = true; $get_posts = new WP_Query(); return $get_posts->query( $parsed_args ); } // // Post meta functions. // /** * Adds a meta field to the given post. * * Post meta data is called "Custom Fields" on the Administration Screen. * * @since 1.5.0 * * @param int $post_id Post ID. * @param string $meta_key Metadata name. * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. * @param bool $unique Optional. Whether the same key should not be added. * Default false. * @return int|false Meta ID on success, false on failure. */ function add_post_meta( $post_id, $meta_key, $meta_value, $unique = false ) { // Make sure meta is added to the post, not a revision. $the_post = wp_is_post_revision( $post_id ); if ( $the_post ) { $post_id = $the_post; } return add_metadata( 'post', $post_id, $meta_key, $meta_value, $unique ); } /** * Deletes a post meta field for the given post ID. * * You can match based on the key, or key and value. Removing based on key and * value, will keep from removing duplicate metadata with the same key. It also * allows removing all metadata matching the key, if needed. * * @since 1.5.0 * * @param int $post_id Post ID. * @param string $meta_key Metadata name. * @param mixed $meta_value Optional. Metadata value. If provided, * rows will only be removed that match the value. * Must be serializable if non-scalar. Default empty. * @return bool True on success, false on failure. */ function delete_post_meta( $post_id, $meta_key, $meta_value = '' ) { // Make sure meta is deleted from the post, not from a revision. $the_post = wp_is_post_revision( $post_id ); if ( $the_post ) { $post_id = $the_post; } return delete_metadata( 'post', $post_id, $meta_key, $meta_value ); } /** * Retrieves a post meta field for the given post ID. * * @since 1.5.0 * * @param int $post_id Post ID. * @param string $key Optional. The meta key to retrieve. By default, * returns data for all keys. Default empty. * @param bool $single Optional. Whether to return a single value. * This parameter has no effect if `$key` is not specified. * Default false. * @return mixed An array of values if `$single` is false. * The value of the meta field if `$single` is true. * False for an invalid `$post_id` (non-numeric, zero, or negative value). * An empty string if a valid but non-existing post ID is passed. */ function get_post_meta( $post_id, $key = '', $single = false ) { return get_metadata( 'post', $post_id, $key, $single ); } /** * Updates a post meta field based on the given post ID. * * Use the `$prev_value` parameter to differentiate between meta fields with the * same key and post ID. * * If the meta field for the post does not exist, it will be added and its ID returned. * * Can be used in place of add_post_meta(). * * @since 1.5.0 * * @param int $post_id Post ID. * @param string $meta_key Metadata key. * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. * @param mixed $prev_value Optional. Previous value to check before updating. * If specified, only update existing metadata entries with * this value. Otherwise, update all entries. Default empty. * @return int|bool Meta ID if the key didn't exist, true on successful update, * false on failure or if the value passed to the function * is the same as the one that is already in the database. */ function update_post_meta( $post_id, $meta_key, $meta_value, $prev_value = '' ) { // Make sure meta is updated for the post, not for a revision. $the_post = wp_is_post_revision( $post_id ); if ( $the_post ) { $post_id = $the_post; } return update_metadata( 'post', $post_id, $meta_key, $meta_value, $prev_value ); } /** * Deletes everything from post meta matching the given meta key. * * @since 2.3.0 * * @param string $post_meta_key Key to search for when deleting. * @return bool Whether the post meta key was deleted from the database. */ function delete_post_meta_by_key( $post_meta_key ) { return delete_metadata( 'post', null, $post_meta_key, '', true ); } /** * Registers a meta key for posts. * * @since 4.9.8 * * @param string $post_type Post type to register a meta key for. Pass an empty string * to register the meta key across all existing post types. * @param string $meta_key The meta key to register. * @param array $args Data used to describe the meta key when registered. See * {@see register_meta()} for a list of supported arguments. * @return bool True if the meta key was successfully registered, false if not. */ function register_post_meta( $post_type, $meta_key, array $args ) { $args['object_subtype'] = $post_type; return register_meta( 'post', $meta_key, $args ); } /** * Unregisters a meta key for posts. * * @since 4.9.8 * * @param string $post_type Post type the meta key is currently registered for. Pass * an empty string if the meta key is registered across all * existing post types. * @param string $meta_key The meta key to unregister. * @return bool True on success, false if the meta key was not previously registered. */ function unregister_post_meta( $post_type, $meta_key ) { return unregister_meta_key( 'post', $meta_key, $post_type ); } /** * Retrieves post meta fields, based on post ID. * * The post meta fields are retrieved from the cache where possible, * so the function is optimized to be called more than once. * * @since 1.2.0 * * @param int $post_id Optional. Post ID. Default is the ID of the global `$post`. * @return mixed An array of values. * False for an invalid `$post_id` (non-numeric, zero, or negative value). * An empty string if a valid but non-existing post ID is passed. */ function get_post_custom( $post_id = 0 ) { $post_id = absint( $post_id ); if ( ! $post_id ) { $post_id = get_the_ID(); } return get_post_meta( $post_id ); } /** * Retrieves meta field names for a post. * * If there are no meta fields, then nothing (null) will be returned. * * @since 1.2.0 * * @param int $post_id Optional. Post ID. Default is the ID of the global `$post`. * @return array|void Array of the keys, if retrieved. */ function get_post_custom_keys( $post_id = 0 ) { $custom = get_post_custom( $post_id ); if ( ! is_array( $custom ) ) { return; } $keys = array_keys( $custom ); if ( $keys ) { return $keys; } } /** * Retrieves values for a custom post field. * * The parameters must not be considered optional. All of the post meta fields * will be retrieved and only the meta field key values returned. * * @since 1.2.0 * * @param string $key Optional. Meta field key. Default empty. * @param int $post_id Optional. Post ID. Default is the ID of the global `$post`. * @return array|null Meta field values. */ function get_post_custom_values( $key = '', $post_id = 0 ) { if ( ! $key ) { return null; } $custom = get_post_custom( $post_id ); return isset( $custom[ $key ] ) ? $custom[ $key ] : null; } /** * Determines whether a post is sticky. * * Sticky posts should remain at the top of The Loop. If the post ID is not * given, then The Loop ID for the current post will be used. * * For more information on this and similar theme functions, check out * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ * Conditional Tags} article in the Theme Developer Handbook. * * @since 2.7.0 * * @param int $post_id Optional. Post ID. Default is the ID of the global `$post`. * @return bool Whether post is sticky. */ function is_sticky( $post_id = 0 ) { $post_id = absint( $post_id ); if ( ! $post_id ) { $post_id = get_the_ID(); } $stickies = get_option( 'sticky_posts' ); if ( is_array( $stickies ) ) { $stickies = array_map( 'intval', $stickies ); $is_sticky = in_array( $post_id, $stickies, true ); } else { $is_sticky = false; } /** * Filters whether a post is sticky. * * @since 5.3.0 * * @param bool $is_sticky Whether a post is sticky. * @param int $post_id Post ID. */ return apply_filters( 'is_sticky', $is_sticky, $post_id ); } /** * Sanitizes every post field. * * If the context is 'raw', then the post object or array will get minimal * sanitization of the integer fields. * * @since 2.3.0 * * @see sanitize_post_field() * * @param object|WP_Post|array $post The post object or array * @param string $context Optional. How to sanitize post fields. * Accepts 'raw', 'edit', 'db', 'display', * 'attribute', or 'js'. Default 'display'. * @return object|WP_Post|array The now sanitized post object or array (will be the * same type as `$post`). */ function sanitize_post( $post, $context = 'display' ) { if ( is_object( $post ) ) { // Check if post already filtered for this context. if ( isset( $post->filter ) && $context == $post->filter ) { return $post; } if ( ! isset( $post->ID ) ) { $post->ID = 0; } foreach ( array_keys( get_object_vars( $post ) ) as $field ) { $post->$field = sanitize_post_field( $field, $post->$field, $post->ID, $context ); } $post->filter = $context; } elseif ( is_array( $post ) ) { // Check if post already filtered for this context. if ( isset( $post['filter'] ) && $context == $post['filter'] ) { return $post; } if ( ! isset( $post['ID'] ) ) { $post['ID'] = 0; } foreach ( array_keys( $post ) as $field ) { $post[ $field ] = sanitize_post_field( $field, $post[ $field ], $post['ID'], $context ); } $post['filter'] = $context; } return $post; } /** * Sanitizes a post field based on context. * * Possible context values are: 'raw', 'edit', 'db', 'display', 'attribute' and * 'js'. The 'display' context is used by default. 'attribute' and 'js' contexts * are treated like 'display' when calling filters. * * @since 2.3.0 * @since 4.4.0 Like `sanitize_post()`, `$context` defaults to 'display'. * * @param string $field The Post Object field name. * @param mixed $value The Post Object value. * @param int $post_id Post ID. * @param string $context Optional. How to sanitize the field. Possible values are 'raw', 'edit', * 'db', 'display', 'attribute' and 'js'. Default 'display'. * @return mixed Sanitized value. */ function sanitize_post_field( $field, $value, $post_id, $context = 'display' ) { $int_fields = array( 'ID', 'post_parent', 'menu_order' ); if ( in_array( $field, $int_fields, true ) ) { $value = (int) $value; } // Fields which contain arrays of integers. $array_int_fields = array( 'ancestors' ); if ( in_array( $field, $array_int_fields, true ) ) { $value = array_map( 'absint', $value ); return $value; } if ( 'raw' === $context ) { return $value; } $prefixed = false; if ( str_contains( $field, 'post_' ) ) { $prefixed = true; $field_no_prefix = str_replace( 'post_', '', $field ); } if ( 'edit' === $context ) { $format_to_edit = array( 'post_content', 'post_excerpt', 'post_title', 'post_password' ); if ( $prefixed ) { /** * Filters the value of a specific post field to edit. * * The dynamic portion of the hook name, `$field`, refers to the post * field name. * * @since 2.3.0 * * @param mixed $value Value of the post field. * @param int $post_id Post ID. */ $value = apply_filters( "edit_{$field}", $value, $post_id ); /** * Filters the value of a specific post field to edit. * * The dynamic portion of the hook name, `$field_no_prefix`, refers to * the post field name. * * @since 2.3.0 * * @param mixed $value Value of the post field. * @param int $post_id Post ID. */ $value = apply_filters( "{$field_no_prefix}_edit_pre", $value, $post_id ); } else { $value = apply_filters( "edit_post_{$field}", $value, $post_id ); } if ( in_array( $field, $format_to_edit, true ) ) { if ( 'post_content' === $field ) { $value = format_to_edit( $value, user_can_richedit() ); } else { $value = format_to_edit( $value ); } } else { $value = esc_attr( $value ); } } elseif ( 'db' === $context ) { if ( $prefixed ) { /** * Filters the value of a specific post field before saving. * * The dynamic portion of the hook name, `$field`, refers to the post * field name. * * @since 2.3.0 * * @param mixed $value Value of the post field. */ $value = apply_filters( "pre_{$field}", $value ); /** * Filters the value of a specific field before saving. * * The dynamic portion of the hook name, `$field_no_prefix`, refers * to the post field name. * * @since 2.3.0 * * @param mixed $value Value of the post field. */ $value = apply_filters( "{$field_no_prefix}_save_pre", $value ); } else { $value = apply_filters( "pre_post_{$field}", $value ); /** * Filters the value of a specific post field before saving. * * The dynamic portion of the hook name, `$field`, refers to the post * field name. * * @since 2.3.0 * * @param mixed $value Value of the post field. */ $value = apply_filters( "{$field}_pre", $value ); } } else { // Use display filters by default. if ( $prefixed ) { /** * Filters the value of a specific post field for display. * * The dynamic portion of the hook name, `$field`, refers to the post * field name. * * @since 2.3.0 * * @param mixed $value Value of the prefixed post field. * @param int $post_id Post ID. * @param string $context Context for how to sanitize the field. * Accepts 'raw', 'edit', 'db', 'display', * 'attribute', or 'js'. Default 'display'. */ $value = apply_filters( "{$field}", $value, $post_id, $context ); } else { $value = apply_filters( "post_{$field}", $value, $post_id, $context ); } if ( 'attribute' === $context ) { $value = esc_attr( $value ); } elseif ( 'js' === $context ) { $value = esc_js( $value ); } } // Restore the type for integer fields after esc_attr(). if ( in_array( $field, $int_fields, true ) ) { $value = (int) $value; } return $value; } /** * Makes a post sticky. * * Sticky posts should be displayed at the top of the front page. * * @since 2.7.0 * * @param int $post_id Post ID. */ function stick_post( $post_id ) { $post_id = (int) $post_id; $stickies = get_option( 'sticky_posts' ); $updated = false; if ( ! is_array( $stickies ) ) { $stickies = array(); } else { $stickies = array_unique( array_map( 'intval', $stickies ) ); } if ( ! in_array( $post_id, $stickies, true ) ) { $stickies[] = $post_id; $updated = update_option( 'sticky_posts', array_values( $stickies ) ); } if ( $updated ) { /** * Fires once a post has been added to the sticky list. * * @since 4.6.0 * * @param int $post_id ID of the post that was stuck. */ do_action( 'post_stuck', $post_id ); } } /** * Un-sticks a post. * * Sticky posts should be displayed at the top of the front page. * * @since 2.7.0 * * @param int $post_id Post ID. */ function unstick_post( $post_id ) { $post_id = (int) $post_id; $stickies = get_option( 'sticky_posts' ); if ( ! is_array( $stickies ) ) { return; } $stickies = array_values( array_unique( array_map( 'intval', $stickies ) ) ); if ( ! in_array( $post_id, $stickies, true ) ) { return; } $offset = array_search( $post_id, $stickies, true ); if ( false === $offset ) { return; } array_splice( $stickies, $offset, 1 ); $updated = update_option( 'sticky_posts', $stickies ); if ( $updated ) { /** * Fires once a post has been removed from the sticky list. * * @since 4.6.0 * * @param int $post_id ID of the post that was unstuck. */ do_action( 'post_unstuck', $post_id ); } } /** * Returns the cache key for wp_count_posts() based on the passed arguments. * * @since 3.9.0 * @access private * * @param string $type Optional. Post type to retrieve count Default 'post'. * @param string $perm Optional. 'readable' or empty. Default empty. * @return string The cache key. */ function _count_posts_cache_key( $type = 'post', $perm = '' ) { $cache_key = 'posts-' . $type; if ( 'readable' === $perm && is_user_logged_in() ) { $post_type_object = get_post_type_object( $type ); if ( $post_type_object && ! current_user_can( $post_type_object->cap->read_private_posts ) ) { $cache_key .= '_' . $perm . '_' . get_current_user_id(); } } return $cache_key; } /** * Counts number of posts of a post type and if user has permissions to view. * * This function provides an efficient method of finding the amount of post's * type a blog has. Another method is to count the amount of items in * get_posts(), but that method has a lot of overhead with doing so. Therefore, * when developing for 2.5+, use this function instead. * * The $perm parameter checks for 'readable' value and if the user can read * private posts, it will display that for the user that is signed in. * * @since 2.5.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $type Optional. Post type to retrieve count. Default 'post'. * @param string $perm Optional. 'readable' or empty. Default empty. * @return stdClass An object containing the number of posts for each status, * or an empty object if the post type does not exist. */ function wp_count_posts( $type = 'post', $perm = '' ) { global $wpdb; if ( ! post_type_exists( $type ) ) { return new stdClass(); } $cache_key = _count_posts_cache_key( $type, $perm ); $counts = wp_cache_get( $cache_key, 'counts' ); if ( false !== $counts ) { // We may have cached this before every status was registered. foreach ( get_post_stati() as $status ) { if ( ! isset( $counts->{$status} ) ) { $counts->{$status} = 0; } } /** This filter is documented in wp-includes/post.php */ return apply_filters( 'wp_count_posts', $counts, $type, $perm ); } $query = "SELECT post_status, COUNT( * ) AS num_posts FROM {$wpdb->posts} WHERE post_type = %s"; if ( 'readable' === $perm && is_user_logged_in() ) { $post_type_object = get_post_type_object( $type ); if ( ! current_user_can( $post_type_object->cap->read_private_posts ) ) { $query .= $wpdb->prepare( " AND (post_status != 'private' OR ( post_author = %d AND post_status = 'private' ))", get_current_user_id() ); } } $query .= ' GROUP BY post_status'; $results = (array) $wpdb->get_results( $wpdb->prepare( $query, $type ), ARRAY_A ); $counts = array_fill_keys( get_post_stati(), 0 ); foreach ( $results as $row ) { $counts[ $row['post_status'] ] = $row['num_posts']; } $counts = (object) $counts; wp_cache_set( $cache_key, $counts, 'counts' ); /** * Filters the post counts by status for the current post type. * * @since 3.7.0 * * @param stdClass $counts An object containing the current post_type's post * counts by status. * @param string $type Post type. * @param string $perm The permission to determine if the posts are 'readable' * by the current user. */ return apply_filters( 'wp_count_posts', $counts, $type, $perm ); } /** * Counts number of attachments for the mime type(s). * * If you set the optional mime_type parameter, then an array will still be * returned, but will only have the item you are looking for. It does not give * you the number of attachments that are children of a post. You can get that * by counting the number of children that post has. * * @since 2.5.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string|string[] $mime_type Optional. Array or comma-separated list of * MIME patterns. Default empty. * @return stdClass An object containing the attachment counts by mime type. */ function wp_count_attachments( $mime_type = '' ) { global $wpdb; $cache_key = sprintf( 'attachments%s', ! empty( $mime_type ) ? ':' . str_replace( '/', '_', implode( '-', (array) $mime_type ) ) : '' ); $counts = wp_cache_get( $cache_key, 'counts' ); if ( false == $counts ) { $and = wp_post_mime_type_where( $mime_type ); $count = $wpdb->get_results( "SELECT post_mime_type, COUNT( * ) AS num_posts FROM $wpdb->posts WHERE post_type = 'attachment' AND post_status != 'trash' $and GROUP BY post_mime_type", ARRAY_A ); $counts = array(); foreach ( (array) $count as $row ) { $counts[ $row['post_mime_type'] ] = $row['num_posts']; } $counts['trash'] = $wpdb->get_var( "SELECT COUNT( * ) FROM $wpdb->posts WHERE post_type = 'attachment' AND post_status = 'trash' $and" ); wp_cache_set( $cache_key, (object) $counts, 'counts' ); } /** * Filters the attachment counts by mime type. * * @since 3.7.0 * * @param stdClass $counts An object containing the attachment counts by * mime type. * @param string|string[] $mime_type Array or comma-separated list of MIME patterns. */ return apply_filters( 'wp_count_attachments', (object) $counts, $mime_type ); } /** * Gets default post mime types. * * @since 2.9.0 * @since 5.3.0 Added the 'Documents', 'Spreadsheets', and 'Archives' mime type groups. * * @return array List of post mime types. */ function get_post_mime_types() { $post_mime_types = array( // array( adj, noun ) 'image' => array( __( 'Images' ), __( 'Manage Images' ), /* translators: %s: Number of images. */ _n_noop( 'Image (%s)', 'Images (%s)' ), ), 'audio' => array( _x( 'Audio', 'file type group' ), __( 'Manage Audio' ), /* translators: %s: Number of audio files. */ _n_noop( 'Audio (%s)', 'Audio (%s)' ), ), 'video' => array( _x( 'Video', 'file type group' ), __( 'Manage Video' ), /* translators: %s: Number of video files. */ _n_noop( 'Video (%s)', 'Video (%s)' ), ), 'document' => array( __( 'Documents' ), __( 'Manage Documents' ), /* translators: %s: Number of documents. */ _n_noop( 'Document (%s)', 'Documents (%s)' ), ), 'spreadsheet' => array( __( 'Spreadsheets' ), __( 'Manage Spreadsheets' ), /* translators: %s: Number of spreadsheets. */ _n_noop( 'Spreadsheet (%s)', 'Spreadsheets (%s)' ), ), 'archive' => array( _x( 'Archives', 'file type group' ), __( 'Manage Archives' ), /* translators: %s: Number of archives. */ _n_noop( 'Archive (%s)', 'Archives (%s)' ), ), ); $ext_types = wp_get_ext_types(); $mime_types = wp_get_mime_types(); foreach ( $post_mime_types as $group => $labels ) { if ( in_array( $group, array( 'image', 'audio', 'video' ), true ) ) { continue; } if ( ! isset( $ext_types[ $group ] ) ) { unset( $post_mime_types[ $group ] ); continue; } $group_mime_types = array(); foreach ( $ext_types[ $group ] as $extension ) { foreach ( $mime_types as $exts => $mime ) { if ( preg_match( '!^(' . $exts . ')$!i', $extension ) ) { $group_mime_types[] = $mime; break; } } } $group_mime_types = implode( ',', array_unique( $group_mime_types ) ); $post_mime_types[ $group_mime_types ] = $labels; unset( $post_mime_types[ $group ] ); } /** * Filters the default list of post mime types. * * @since 2.5.0 * * @param array $post_mime_types Default list of post mime types. */ return apply_filters( 'post_mime_types', $post_mime_types ); } /** * Checks a MIME-Type against a list. * * If the `$wildcard_mime_types` parameter is a string, it must be comma separated * list. If the `$real_mime_types` is a string, it is also comma separated to * create the list. * * @since 2.5.0 * * @param string|string[] $wildcard_mime_types Mime types, e.g. `audio/mpeg`, `image` (same as `image/*`), * or `flash` (same as `*flash*`). * @param string|string[] $real_mime_types Real post mime type values. * @return array array(wildcard=>array(real types)). */ function wp_match_mime_types( $wildcard_mime_types, $real_mime_types ) { $matches = array(); if ( is_string( $wildcard_mime_types ) ) { $wildcard_mime_types = array_map( 'trim', explode( ',', $wildcard_mime_types ) ); } if ( is_string( $real_mime_types ) ) { $real_mime_types = array_map( 'trim', explode( ',', $real_mime_types ) ); } $patternses = array(); $wild = '[-._a-z0-9]*'; foreach ( (array) $wildcard_mime_types as $type ) { $mimes = array_map( 'trim', explode( ',', $type ) ); foreach ( $mimes as $mime ) { $regex = str_replace( '__wildcard__', $wild, preg_quote( str_replace( '*', '__wildcard__', $mime ) ) ); $patternses[][ $type ] = "^$regex$"; if ( ! str_contains( $mime, '/' ) ) { $patternses[][ $type ] = "^$regex/"; $patternses[][ $type ] = $regex; } } } asort( $patternses ); foreach ( $patternses as $patterns ) { foreach ( $patterns as $type => $pattern ) { foreach ( (array) $real_mime_types as $real ) { if ( preg_match( "#$pattern#", $real ) && ( empty( $matches[ $type ] ) || false === array_search( $real, $matches[ $type ], true ) ) ) { $matches[ $type ][] = $real; } } } } return $matches; } /** * Converts MIME types into SQL. * * @since 2.5.0 * * @param string|string[] $post_mime_types List of mime types or comma separated string * of mime types. * @param string $table_alias Optional. Specify a table alias, if needed. * Default empty. * @return string The SQL AND clause for mime searching. */ function wp_post_mime_type_where( $post_mime_types, $table_alias = '' ) { $where = ''; $wildcards = array( '', '%', '%/%' ); if ( is_string( $post_mime_types ) ) { $post_mime_types = array_map( 'trim', explode( ',', $post_mime_types ) ); } $wheres = array(); foreach ( (array) $post_mime_types as $mime_type ) { $mime_type = preg_replace( '/\s/', '', $mime_type ); $slashpos = strpos( $mime_type, '/' ); if ( false !== $slashpos ) { $mime_group = preg_replace( '/[^-*.a-zA-Z0-9]/', '', substr( $mime_type, 0, $slashpos ) ); $mime_subgroup = preg_replace( '/[^-*.+a-zA-Z0-9]/', '', substr( $mime_type, $slashpos + 1 ) ); if ( empty( $mime_subgroup ) ) { $mime_subgroup = '*'; } else { $mime_subgroup = str_replace( '/', '', $mime_subgroup ); } $mime_pattern = "$mime_group/$mime_subgroup"; } else { $mime_pattern = preg_replace( '/[^-*.a-zA-Z0-9]/', '', $mime_type ); if ( ! str_contains( $mime_pattern, '*' ) ) { $mime_pattern .= '/*'; } } $mime_pattern = preg_replace( '/\*+/', '%', $mime_pattern ); if ( in_array( $mime_type, $wildcards, true ) ) { return ''; } if ( str_contains( $mime_pattern, '%' ) ) { $wheres[] = empty( $table_alias ) ? "post_mime_type LIKE '$mime_pattern'" : "$table_alias.post_mime_type LIKE '$mime_pattern'"; } else { $wheres[] = empty( $table_alias ) ? "post_mime_type = '$mime_pattern'" : "$table_alias.post_mime_type = '$mime_pattern'"; } } if ( ! empty( $wheres ) ) { $where = ' AND (' . implode( ' OR ', $wheres ) . ') '; } return $where; } /** * Trashes or deletes a post or page. * * When the post and page is permanently deleted, everything that is tied to * it is deleted also. This includes comments, post meta fields, and terms * associated with the post. * * The post or page is moved to Trash instead of permanently deleted unless * Trash is disabled, item is already in the Trash, or $force_delete is true. * * @since 1.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * @see wp_delete_attachment() * @see wp_trash_post() * * @param int $postid Optional. Post ID. Default 0. * @param bool $force_delete Optional. Whether to bypass Trash and force deletion. * Default false. * @return WP_Post|false|null Post data on success, false or null on failure. */ function wp_delete_post( $postid = 0, $force_delete = false ) { global $wpdb; $post = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE ID = %d", $postid ) ); if ( ! $post ) { return $post; } $post = get_post( $post ); if ( ! $force_delete && ( 'post' === $post->post_type || 'page' === $post->post_type ) && 'trash' !== get_post_status( $postid ) && EMPTY_TRASH_DAYS ) { return wp_trash_post( $postid ); } if ( 'attachment' === $post->post_type ) { return wp_delete_attachment( $postid, $force_delete ); } /** * Filters whether a post deletion should take place. * * @since 4.4.0 * * @param WP_Post|false|null $delete Whether to go forward with deletion. * @param WP_Post $post Post object. * @param bool $force_delete Whether to bypass the Trash. */ $check = apply_filters( 'pre_delete_post', null, $post, $force_delete ); if ( null !== $check ) { return $check; } /** * Fires before a post is deleted, at the start of wp_delete_post(). * * @since 3.2.0 * @since 5.5.0 Added the `$post` parameter. * * @see wp_delete_post() * * @param int $postid Post ID. * @param WP_Post $post Post object. */ do_action( 'before_delete_post', $postid, $post ); delete_post_meta( $postid, '_wp_trash_meta_status' ); delete_post_meta( $postid, '_wp_trash_meta_time' ); wp_delete_object_term_relationships( $postid, get_object_taxonomies( $post->post_type ) ); $parent_data = array( 'post_parent' => $post->post_parent ); $parent_where = array( 'post_parent' => $postid ); if ( is_post_type_hierarchical( $post->post_type ) ) { // Point children of this page to its parent, also clean the cache of affected children. $children_query = $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_parent = %d AND post_type = %s", $postid, $post->post_type ); $children = $wpdb->get_results( $children_query ); if ( $children ) { $wpdb->update( $wpdb->posts, $parent_data, $parent_where + array( 'post_type' => $post->post_type ) ); } } // Do raw query. wp_get_post_revisions() is filtered. $revision_ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_parent = %d AND post_type = 'revision'", $postid ) ); // Use wp_delete_post (via wp_delete_post_revision) again. Ensures any meta/misplaced data gets cleaned up. foreach ( $revision_ids as $revision_id ) { wp_delete_post_revision( $revision_id ); } // Point all attachments to this post up one level. $wpdb->update( $wpdb->posts, $parent_data, $parent_where + array( 'post_type' => 'attachment' ) ); wp_defer_comment_counting( true ); $comment_ids = $wpdb->get_col( $wpdb->prepare( "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = %d ORDER BY comment_ID DESC", $postid ) ); foreach ( $comment_ids as $comment_id ) { wp_delete_comment( $comment_id, true ); } wp_defer_comment_counting( false ); $post_meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE post_id = %d ", $postid ) ); foreach ( $post_meta_ids as $mid ) { delete_metadata_by_mid( 'post', $mid ); } /** * Fires immediately before a post is deleted from the database. * * @since 1.2.0 * @since 5.5.0 Added the `$post` parameter. * * @param int $postid Post ID. * @param WP_Post $post Post object. */ do_action( 'delete_post', $postid, $post ); $result = $wpdb->delete( $wpdb->posts, array( 'ID' => $postid ) ); if ( ! $result ) { return false; } /** * Fires immediately after a post is deleted from the database. * * @since 2.2.0 * @since 5.5.0 Added the `$post` parameter. * * @param int $postid Post ID. * @param WP_Post $post Post object. */ do_action( 'deleted_post', $postid, $post ); clean_post_cache( $post ); if ( is_post_type_hierarchical( $post->post_type ) && $children ) { foreach ( $children as $child ) { clean_post_cache( $child ); } } wp_clear_scheduled_hook( 'publish_future_post', array( $postid ) ); /** * Fires after a post is deleted, at the conclusion of wp_delete_post(). * * @since 3.2.0 * @since 5.5.0 Added the `$post` parameter. * * @see wp_delete_post() * * @param int $postid Post ID. * @param WP_Post $post Post object. */ do_action( 'after_delete_post', $postid, $post ); return $post; } /** * Resets the page_on_front, show_on_front, and page_for_post settings when * a linked page is deleted or trashed. * * Also ensures the post is no longer sticky. * * @since 3.7.0 * @access private * * @param int $post_id Post ID. */ function _reset_front_page_settings_for_post( $post_id ) { $post = get_post( $post_id ); if ( 'page' === $post->post_type ) { /* * If the page is defined in option page_on_front or post_for_posts, * adjust the corresponding options. */ if ( get_option( 'page_on_front' ) == $post->ID ) { update_option( 'show_on_front', 'posts' ); update_option( 'page_on_front', 0 ); } if ( get_option( 'page_for_posts' ) == $post->ID ) { update_option( 'page_for_posts', 0 ); } } unstick_post( $post->ID ); } /** * Moves a post or page to the Trash * * If Trash is disabled, the post or page is permanently deleted. * * @since 2.9.0 * * @see wp_delete_post() * * @param int $post_id Optional. Post ID. Default is the ID of the global `$post` * if `EMPTY_TRASH_DAYS` equals true. * @return WP_Post|false|null Post data on success, false or null on failure. */ function wp_trash_post( $post_id = 0 ) { if ( ! EMPTY_TRASH_DAYS ) { return wp_delete_post( $post_id, true ); } $post = get_post( $post_id ); if ( ! $post ) { return $post; } if ( 'trash' === $post->post_status ) { return false; } $previous_status = $post->post_status; /** * Filters whether a post trashing should take place. * * @since 4.9.0 * @since 6.3.0 Added the `$previous_status` parameter. * * @param bool|null $trash Whether to go forward with trashing. * @param WP_Post $post Post object. * @param string $previous_status The status of the post about to be trashed. */ $check = apply_filters( 'pre_trash_post', null, $post, $previous_status ); if ( null !== $check ) { return $check; } /** * Fires before a post is sent to the Trash. * * @since 3.3.0 * @since 6.3.0 Added the `$previous_status` parameter. * * @param int $post_id Post ID. * @param string $previous_status The status of the post about to be trashed. */ do_action( 'wp_trash_post', $post_id, $previous_status ); add_post_meta( $post_id, '_wp_trash_meta_status', $previous_status ); add_post_meta( $post_id, '_wp_trash_meta_time', time() ); $post_updated = wp_update_post( array( 'ID' => $post_id, 'post_status' => 'trash', ) ); if ( ! $post_updated ) { return false; } wp_trash_post_comments( $post_id ); /** * Fires after a post is sent to the Trash. * * @since 2.9.0 * @since 6.3.0 Added the `$previous_status` parameter. * * @param int $post_id Post ID. * @param string $previous_status The status of the post at the point where it was trashed. */ do_action( 'trashed_post', $post_id, $previous_status ); return $post; } /** * Restores a post from the Trash. * * @since 2.9.0 * @since 5.6.0 An untrashed post is now returned to 'draft' status by default, except for * attachments which are returned to their original 'inherit' status. * * @param int $post_id Optional. Post ID. Default is the ID of the global `$post`. * @return WP_Post|false|null Post data on success, false or null on failure. */ function wp_untrash_post( $post_id = 0 ) { $post = get_post( $post_id ); if ( ! $post ) { return $post; } $post_id = $post->ID; if ( 'trash' !== $post->post_status ) { return false; } $previous_status = get_post_meta( $post_id, '_wp_trash_meta_status', true ); /** * Filters whether a post untrashing should take place. * * @since 4.9.0 * @since 5.6.0 Added the `$previous_status` parameter. * * @param bool|null $untrash Whether to go forward with untrashing. * @param WP_Post $post Post object. * @param string $previous_status The status of the post at the point where it was trashed. */ $check = apply_filters( 'pre_untrash_post', null, $post, $previous_status ); if ( null !== $check ) { return $check; } /** * Fires before a post is restored from the Trash. * * @since 2.9.0 * @since 5.6.0 Added the `$previous_status` parameter. * * @param int $post_id Post ID. * @param string $previous_status The status of the post at the point where it was trashed. */ do_action( 'untrash_post', $post_id, $previous_status ); $new_status = ( 'attachment' === $post->post_type ) ? 'inherit' : 'draft'; /** * Filters the status that a post gets assigned when it is restored from the trash (untrashed). * * By default posts that are restored will be assigned a status of 'draft'. Return the value of `$previous_status` * in order to assign the status that the post had before it was trashed. The `wp_untrash_post_set_previous_status()` * function is available for this. * * Prior to WordPress 5.6.0, restored posts were always assigned their original status. * * @since 5.6.0 * * @param string $new_status The new status of the post being restored. * @param int $post_id The ID of the post being restored. * @param string $previous_status The status of the post at the point where it was trashed. */ $post_status = apply_filters( 'wp_untrash_post_status', $new_status, $post_id, $previous_status ); delete_post_meta( $post_id, '_wp_trash_meta_status' ); delete_post_meta( $post_id, '_wp_trash_meta_time' ); $post_updated = wp_update_post( array( 'ID' => $post_id, 'post_status' => $post_status, ) ); if ( ! $post_updated ) { return false; } wp_untrash_post_comments( $post_id ); /** * Fires after a post is restored from the Trash. * * @since 2.9.0 * @since 5.6.0 Added the `$previous_status` parameter. * * @param int $post_id Post ID. * @param string $previous_status The status of the post at the point where it was trashed. */ do_action( 'untrashed_post', $post_id, $previous_status ); return $post; } /** * Moves comments for a post to the Trash. * * @since 2.9.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int|WP_Post|null $post Optional. Post ID or post object. Defaults to global $post. * @return mixed|void False on failure. */ function wp_trash_post_comments( $post = null ) { global $wpdb; $post = get_post( $post ); if ( ! $post ) { return; } $post_id = $post->ID; /** * Fires before comments are sent to the Trash. * * @since 2.9.0 * * @param int $post_id Post ID. */ do_action( 'trash_post_comments', $post_id ); $comments = $wpdb->get_results( $wpdb->prepare( "SELECT comment_ID, comment_approved FROM $wpdb->comments WHERE comment_post_ID = %d", $post_id ) ); if ( ! $comments ) { return; } // Cache current status for each comment. $statuses = array(); foreach ( $comments as $comment ) { $statuses[ $comment->comment_ID ] = $comment->comment_approved; } add_post_meta( $post_id, '_wp_trash_meta_comments_status', $statuses ); // Set status for all comments to post-trashed. $result = $wpdb->update( $wpdb->comments, array( 'comment_approved' => 'post-trashed' ), array( 'comment_post_ID' => $post_id ) ); clean_comment_cache( array_keys( $statuses ) ); /** * Fires after comments are sent to the Trash. * * @since 2.9.0 * * @param int $post_id Post ID. * @param array $statuses Array of comment statuses. */ do_action( 'trashed_post_comments', $post_id, $statuses ); return $result; } /** * Restores comments for a post from the Trash. * * @since 2.9.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int|WP_Post|null $post Optional. Post ID or post object. Defaults to global $post. * @return true|void */ function wp_untrash_post_comments( $post = null ) { global $wpdb; $post = get_post( $post ); if ( ! $post ) { return; } $post_id = $post->ID; $statuses = get_post_meta( $post_id, '_wp_trash_meta_comments_status', true ); if ( ! $statuses ) { return true; } /** * Fires before comments are restored for a post from the Trash. * * @since 2.9.0 * * @param int $post_id Post ID. */ do_action( 'untrash_post_comments', $post_id ); // Restore each comment to its original status. $group_by_status = array(); foreach ( $statuses as $comment_id => $comment_status ) { $group_by_status[ $comment_status ][] = $comment_id; } foreach ( $group_by_status as $status => $comments ) { // Sanity check. This shouldn't happen. if ( 'post-trashed' === $status ) { $status = '0'; } $comments_in = implode( ', ', array_map( 'intval', $comments ) ); $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->comments SET comment_approved = %s WHERE comment_ID IN ($comments_in)", $status ) ); } clean_comment_cache( array_keys( $statuses ) ); delete_post_meta( $post_id, '_wp_trash_meta_comments_status' ); /** * Fires after comments are restored for a post from the Trash. * * @since 2.9.0 * * @param int $post_id Post ID. */ do_action( 'untrashed_post_comments', $post_id ); } /** * Retrieves the list of categories for a post. * * Compatibility layer for themes and plugins. Also an easy layer of abstraction * away from the complexity of the taxonomy layer. * * @since 2.1.0 * * @see wp_get_object_terms() * * @param int $post_id Optional. The Post ID. Does not default to the ID of the * global $post. Default 0. * @param array $args Optional. Category query parameters. Default empty array. * See WP_Term_Query::__construct() for supported arguments. * @return array|WP_Error List of categories. If the `$fields` argument passed via `$args` is 'all' or * 'all_with_object_id', an array of WP_Term objects will be returned. If `$fields` * is 'ids', an array of category IDs. If `$fields` is 'names', an array of category names. * WP_Error object if 'category' taxonomy doesn't exist. */ function wp_get_post_categories( $post_id = 0, $args = array() ) { $post_id = (int) $post_id; $defaults = array( 'fields' => 'ids' ); $args = wp_parse_args( $args, $defaults ); $cats = wp_get_object_terms( $post_id, 'category', $args ); return $cats; } /** * Retrieves the tags for a post. * * There is only one default for this function, called 'fields' and by default * is set to 'all'. There are other defaults that can be overridden in * wp_get_object_terms(). * * @since 2.3.0 * * @param int $post_id Optional. The Post ID. Does not default to the ID of the * global $post. Default 0. * @param array $args Optional. Tag query parameters. Default empty array. * See WP_Term_Query::__construct() for supported arguments. * @return array|WP_Error Array of WP_Term objects on success or empty array if no tags were found. * WP_Error object if 'post_tag' taxonomy doesn't exist. */ function wp_get_post_tags( $post_id = 0, $args = array() ) { return wp_get_post_terms( $post_id, 'post_tag', $args ); } /** * Retrieves the terms for a post. * * @since 2.8.0 * * @param int $post_id Optional. The Post ID. Does not default to the ID of the * global $post. Default 0. * @param string|string[] $taxonomy Optional. The taxonomy slug or array of slugs for which * to retrieve terms. Default 'post_tag'. * @param array $args { * Optional. Term query parameters. See WP_Term_Query::__construct() for supported arguments. * * @type string $fields Term fields to retrieve. Default 'all'. * } * @return array|WP_Error Array of WP_Term objects on success or empty array if no terms were found. * WP_Error object if `$taxonomy` doesn't exist. */ function wp_get_post_terms( $post_id = 0, $taxonomy = 'post_tag', $args = array() ) { $post_id = (int) $post_id; $defaults = array( 'fields' => 'all' ); $args = wp_parse_args( $args, $defaults ); $tags = wp_get_object_terms( $post_id, $taxonomy, $args ); return $tags; } /** * Retrieves a number of recent posts. * * @since 1.0.0 * * @see get_posts() * * @param array $args Optional. Arguments to retrieve posts. Default empty array. * @param string $output Optional. The required return type. One of OBJECT or ARRAY_A, which * correspond to a WP_Post object or an associative array, respectively. * Default ARRAY_A. * @return array|false Array of recent posts, where the type of each element is determined * by the `$output` parameter. Empty array on failure. */ function wp_get_recent_posts( $args = array(), $output = ARRAY_A ) { if ( is_numeric( $args ) ) { _deprecated_argument( __FUNCTION__, '3.1.0', __( 'Passing an integer number of posts is deprecated. Pass an array of arguments instead.' ) ); $args = array( 'numberposts' => absint( $args ) ); } // Set default arguments. $defaults = array( 'numberposts' => 10, 'offset' => 0, 'category' => 0, 'orderby' => 'post_date', 'order' => 'DESC', 'include' => '', 'exclude' => '', 'meta_key' => '', 'meta_value' => '', 'post_type' => 'post', 'post_status' => 'draft, publish, future, pending, private', 'suppress_filters' => true, ); $parsed_args = wp_parse_args( $args, $defaults ); $results = get_posts( $parsed_args ); // Backward compatibility. Prior to 3.1 expected posts to be returned in array. if ( ARRAY_A === $output ) { foreach ( $results as $key => $result ) { $results[ $key ] = get_object_vars( $result ); } return $results ? $results : array(); } return $results ? $results : false; } /** * Inserts or update a post. * * If the $postarr parameter has 'ID' set to a value, then post will be updated. * * You can set the post date manually, by setting the values for 'post_date' * and 'post_date_gmt' keys. You can close the comments or open the comments by * setting the value for 'comment_status' key. * * @since 1.0.0 * @since 2.6.0 Added the `$wp_error` parameter to allow a WP_Error to be returned on failure. * @since 4.2.0 Support was added for encoding emoji in the post title, content, and excerpt. * @since 4.4.0 A 'meta_input' array can now be passed to `$postarr` to add post meta data. * @since 5.6.0 Added the `$fire_after_hooks` parameter. * * @see sanitize_post() * @global wpdb $wpdb WordPress database abstraction object. * * @param array $postarr { * An array of elements that make up a post to update or insert. * * @type int $ID The post ID. If equal to something other than 0, * the post with that ID will be updated. Default 0. * @type int $post_author The ID of the user who added the post. Default is * the current user ID. * @type string $post_date The date of the post. Default is the current time. * @type string $post_date_gmt The date of the post in the GMT timezone. Default is * the value of `$post_date`. * @type string $post_content The post content. Default empty. * @type string $post_content_filtered The filtered post content. Default empty. * @type string $post_title The post title. Default empty. * @type string $post_excerpt The post excerpt. Default empty. * @type string $post_status The post status. Default 'draft'. * @type string $post_type The post type. Default 'post'. * @type string $comment_status Whether the post can accept comments. Accepts 'open' or 'closed'. * Default is the value of 'default_comment_status' option. * @type string $ping_status Whether the post can accept pings. Accepts 'open' or 'closed'. * Default is the value of 'default_ping_status' option. * @type string $post_password The password to access the post. Default empty. * @type string $post_name The post name. Default is the sanitized post title * when creating a new post. * @type string $to_ping Space or carriage return-separated list of URLs to ping. * Default empty. * @type string $pinged Space or carriage return-separated list of URLs that have * been pinged. Default empty. * @type int $post_parent Set this for the post it belongs to, if any. Default 0. * @type int $menu_order The order the post should be displayed in. Default 0. * @type string $post_mime_type The mime type of the post. Default empty. * @type string $guid Global Unique ID for referencing the post. Default empty. * @type int $import_id The post ID to be used when inserting a new post. * If specified, must not match any existing post ID. Default 0. * @type int[] $post_category Array of category IDs. * Defaults to value of the 'default_category' option. * @type array $tags_input Array of tag names, slugs, or IDs. Default empty. * @type array $tax_input An array of taxonomy terms keyed by their taxonomy name. * If the taxonomy is hierarchical, the term list needs to be * either an array of term IDs or a comma-separated string of IDs. * If the taxonomy is non-hierarchical, the term list can be an array * that contains term names or slugs, or a comma-separated string * of names or slugs. This is because, in hierarchical taxonomy, * child terms can have the same names with different parent terms, * so the only way to connect them is using ID. Default empty. * @type array $meta_input Array of post meta values keyed by their post meta key. Default empty. * @type string $page_template Page template to use. * } * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false. * @param bool $fire_after_hooks Optional. Whether to fire the after insert hooks. Default true. * @return int|WP_Error The post ID on success. The value 0 or WP_Error on failure. */ function wp_insert_post( $postarr, $wp_error = false, $fire_after_hooks = true ) { global $wpdb; // Capture original pre-sanitized array for passing into filters. $unsanitized_postarr = $postarr; $user_id = get_current_user_id(); $defaults = array( 'post_author' => $user_id, 'post_content' => '', 'post_content_filtered' => '', 'post_title' => '', 'post_excerpt' => '', 'post_status' => 'draft', 'post_type' => 'post', 'comment_status' => '', 'ping_status' => '', 'post_password' => '', 'to_ping' => '', 'pinged' => '', 'post_parent' => 0, 'menu_order' => 0, 'guid' => '', 'import_id' => 0, 'context' => '', 'post_date' => '', 'post_date_gmt' => '', ); $postarr = wp_parse_args( $postarr, $defaults ); unset( $postarr['filter'] ); $postarr = sanitize_post( $postarr, 'db' ); // Are we updating or creating? $post_id = 0; $update = false; $guid = $postarr['guid']; if ( ! empty( $postarr['ID'] ) ) { $update = true; // Get the post ID and GUID. $post_id = $postarr['ID']; $post_before = get_post( $post_id ); if ( is_null( $post_before ) ) { if ( $wp_error ) { return new WP_Error( 'invalid_post', __( 'Invalid post ID.' ) ); } return 0; } $guid = get_post_field( 'guid', $post_id ); $previous_status = get_post_field( 'post_status', $post_id ); } else { $previous_status = 'new'; $post_before = null; } $post_type = empty( $postarr['post_type'] ) ? 'post' : $postarr['post_type']; $post_title = $postarr['post_title']; $post_content = $postarr['post_content']; $post_excerpt = $postarr['post_excerpt']; if ( isset( $postarr['post_name'] ) ) { $post_name = $postarr['post_name']; } elseif ( $update ) { // For an update, don't modify the post_name if it wasn't supplied as an argument. $post_name = $post_before->post_name; } $maybe_empty = 'attachment' !== $post_type && ! $post_content && ! $post_title && ! $post_excerpt && post_type_supports( $post_type, 'editor' ) && post_type_supports( $post_type, 'title' ) && post_type_supports( $post_type, 'excerpt' ); /** * Filters whether the post should be considered "empty". * * The post is considered "empty" if both: * 1. The post type supports the title, editor, and excerpt fields * 2. The title, editor, and excerpt fields are all empty * * Returning a truthy value from the filter will effectively short-circuit * the new post being inserted and return 0. If $wp_error is true, a WP_Error * will be returned instead. * * @since 3.3.0 * * @param bool $maybe_empty Whether the post should be considered "empty". * @param array $postarr Array of post data. */ if ( apply_filters( 'wp_insert_post_empty_content', $maybe_empty, $postarr ) ) { if ( $wp_error ) { return new WP_Error( 'empty_content', __( 'Content, title, and excerpt are empty.' ) ); } else { return 0; } } $post_status = empty( $postarr['post_status'] ) ? 'draft' : $postarr['post_status']; if ( 'attachment' === $post_type && ! in_array( $post_status, array( 'inherit', 'private', 'trash', 'auto-draft' ), true ) ) { $post_status = 'inherit'; } if ( ! empty( $postarr['post_category'] ) ) { // Filter out empty terms. $post_category = array_filter( $postarr['post_category'] ); } elseif ( $update && ! isset( $postarr['post_category'] ) ) { $post_category = $post_before->post_category; } // Make sure we set a valid category. if ( empty( $post_category ) || 0 === count( $post_category ) || ! is_array( $post_category ) ) { // 'post' requires at least one category. if ( 'post' === $post_type && 'auto-draft' !== $post_status ) { $post_category = array( get_option( 'default_category' ) ); } else { $post_category = array(); } } /* * Don't allow contributors to set the post slug for pending review posts. * * For new posts check the primitive capability, for updates check the meta capability. */ if ( 'pending' === $post_status ) { $post_type_object = get_post_type_object( $post_type ); if ( ! $update && $post_type_object && ! current_user_can( $post_type_object->cap->publish_posts ) ) { $post_name = ''; } elseif ( $update && ! current_user_can( 'publish_post', $post_id ) ) { $post_name = ''; } } /* * Create a valid post name. Drafts and pending posts are allowed to have * an empty post name. */ if ( empty( $post_name ) ) { if ( ! in_array( $post_status, array( 'draft', 'pending', 'auto-draft' ), true ) ) { $post_name = sanitize_title( $post_title ); } else { $post_name = ''; } } else { // On updates, we need to check to see if it's using the old, fixed sanitization context. $check_name = sanitize_title( $post_name, '', 'old-save' ); if ( $update && strtolower( urlencode( $post_name ) ) === $check_name && get_post_field( 'post_name', $post_id ) === $check_name ) { $post_name = $check_name; } else { // New post, or slug has changed. $post_name = sanitize_title( $post_name ); } } /* * Resolve the post date from any provided post date or post date GMT strings; * if none are provided, the date will be set to now. */ $post_date = wp_resolve_post_date( $postarr['post_date'], $postarr['post_date_gmt'] ); if ( ! $post_date ) { if ( $wp_error ) { return new WP_Error( 'invalid_date', __( 'Invalid date.' ) ); } else { return 0; } } if ( empty( $postarr['post_date_gmt'] ) || '0000-00-00 00:00:00' === $postarr['post_date_gmt'] ) { if ( ! in_array( $post_status, get_post_stati( array( 'date_floating' => true ) ), true ) ) { $post_date_gmt = get_gmt_from_date( $post_date ); } else { $post_date_gmt = '0000-00-00 00:00:00'; } } else { $post_date_gmt = $postarr['post_date_gmt']; } if ( $update || '0000-00-00 00:00:00' === $post_date ) { $post_modified = current_time( 'mysql' ); $post_modified_gmt = current_time( 'mysql', 1 ); } else { $post_modified = $post_date; $post_modified_gmt = $post_date_gmt; } if ( 'attachment' !== $post_type ) { $now = gmdate( 'Y-m-d H:i:s' ); if ( 'publish' === $post_status ) { if ( strtotime( $post_date_gmt ) - strtotime( $now ) >= MINUTE_IN_SECONDS ) { $post_status = 'future'; } } elseif ( 'future' === $post_status ) { if ( strtotime( $post_date_gmt ) - strtotime( $now ) < MINUTE_IN_SECONDS ) { $post_status = 'publish'; } } } // Comment status. if ( empty( $postarr['comment_status'] ) ) { if ( $update ) { $comment_status = 'closed'; } else { $comment_status = get_default_comment_status( $post_type ); } } else { $comment_status = $postarr['comment_status']; } // These variables are needed by compact() later. $post_content_filtered = $postarr['post_content_filtered']; $post_author = isset( $postarr['post_author'] ) ? $postarr['post_author'] : $user_id; $ping_status = empty( $postarr['ping_status'] ) ? get_default_comment_status( $post_type, 'pingback' ) : $postarr['ping_status']; $to_ping = isset( $postarr['to_ping'] ) ? sanitize_trackback_urls( $postarr['to_ping'] ) : ''; $pinged = isset( $postarr['pinged'] ) ? $postarr['pinged'] : ''; $import_id = isset( $postarr['import_id'] ) ? $postarr['import_id'] : 0; /* * The 'wp_insert_post_parent' filter expects all variables to be present. * Previously, these variables would have already been extracted */ if ( isset( $postarr['menu_order'] ) ) { $menu_order = (int) $postarr['menu_order']; } else { $menu_order = 0; } $post_password = isset( $postarr['post_password'] ) ? $postarr['post_password'] : ''; if ( 'private' === $post_status ) { $post_password = ''; } if ( isset( $postarr['post_parent'] ) ) { $post_parent = (int) $postarr['post_parent']; } else { $post_parent = 0; } $new_postarr = array_merge( array( 'ID' => $post_id, ), compact( array_diff( array_keys( $defaults ), array( 'context', 'filter' ) ) ) ); /** * Filters the post parent -- used to check for and prevent hierarchy loops. * * @since 3.1.0 * * @param int $post_parent Post parent ID. * @param int $post_id Post ID. * @param array $new_postarr Array of parsed post data. * @param array $postarr Array of sanitized, but otherwise unmodified post data. */ $post_parent = apply_filters( 'wp_insert_post_parent', $post_parent, $post_id, $new_postarr, $postarr ); /* * If the post is being untrashed and it has a desired slug stored in post meta, * reassign it. */ if ( 'trash' === $previous_status && 'trash' !== $post_status ) { $desired_post_slug = get_post_meta( $post_id, '_wp_desired_post_slug', true ); if ( $desired_post_slug ) { delete_post_meta( $post_id, '_wp_desired_post_slug' ); $post_name = $desired_post_slug; } } // If a trashed post has the desired slug, change it and let this post have it. if ( 'trash' !== $post_status && $post_name ) { /** * Filters whether or not to add a `__trashed` suffix to trashed posts that match the name of the updated post. * * @since 5.4.0 * * @param bool $add_trashed_suffix Whether to attempt to add the suffix. * @param string $post_name The name of the post being updated. * @param int $post_id Post ID. */ $add_trashed_suffix = apply_filters( 'add_trashed_suffix_to_trashed_posts', true, $post_name, $post_id ); if ( $add_trashed_suffix ) { wp_add_trashed_suffix_to_post_name_for_trashed_posts( $post_name, $post_id ); } } // When trashing an existing post, change its slug to allow non-trashed posts to use it. if ( 'trash' === $post_status && 'trash' !== $previous_status && 'new' !== $previous_status ) { $post_name = wp_add_trashed_suffix_to_post_name_for_post( $post_id ); } $post_name = wp_unique_post_slug( $post_name, $post_id, $post_status, $post_type, $post_parent ); // Don't unslash. $post_mime_type = isset( $postarr['post_mime_type'] ) ? $postarr['post_mime_type'] : ''; // Expected_slashed (everything!). $data = compact( 'post_author', 'post_date', 'post_date_gmt', 'post_content', 'post_content_filtered', 'post_title', 'post_excerpt', 'post_status', 'post_type', 'comment_status', 'ping_status', 'post_password', 'post_name', 'to_ping', 'pinged', 'post_modified', 'post_modified_gmt', 'post_parent', 'menu_order', 'post_mime_type', 'guid' ); $emoji_fields = array( 'post_title', 'post_content', 'post_excerpt' ); foreach ( $emoji_fields as $emoji_field ) { if ( isset( $data[ $emoji_field ] ) ) { $charset = $wpdb->get_col_charset( $wpdb->posts, $emoji_field ); if ( 'utf8' === $charset ) { $data[ $emoji_field ] = wp_encode_emoji( $data[ $emoji_field ] ); } } } if ( 'attachment' === $post_type ) { /** * Filters attachment post data before it is updated in or added to the database. * * @since 3.9.0 * @since 5.4.1 The `$unsanitized_postarr` parameter was added. * @since 6.0.0 The `$update` parameter was added. * * @param array $data An array of slashed, sanitized, and processed attachment post data. * @param array $postarr An array of slashed and sanitized attachment post data, but not processed. * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data * as originally passed to wp_insert_post(). * @param bool $update Whether this is an existing attachment post being updated. */ $data = apply_filters( 'wp_insert_attachment_data', $data, $postarr, $unsanitized_postarr, $update ); } else { /** * Filters slashed post data just before it is inserted into the database. * * @since 2.7.0 * @since 5.4.1 The `$unsanitized_postarr` parameter was added. * @since 6.0.0 The `$update` parameter was added. * * @param array $data An array of slashed, sanitized, and processed post data. * @param array $postarr An array of sanitized (and slashed) but otherwise unmodified post data. * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed post data as * originally passed to wp_insert_post(). * @param bool $update Whether this is an existing post being updated. */ $data = apply_filters( 'wp_insert_post_data', $data, $postarr, $unsanitized_postarr, $update ); } $data = wp_unslash( $data ); $where = array( 'ID' => $post_id ); if ( $update ) { /** * Fires immediately before an existing post is updated in the database. * * @since 2.5.0 * * @param int $post_id Post ID. * @param array $data Array of unslashed post data. */ do_action( 'pre_post_update', $post_id, $data ); if ( false === $wpdb->update( $wpdb->posts, $data, $where ) ) { if ( $wp_error ) { if ( 'attachment' === $post_type ) { $message = __( 'Could not update attachment in the database.' ); } else { $message = __( 'Could not update post in the database.' ); } return new WP_Error( 'db_update_error', $message, $wpdb->last_error ); } else { return 0; } } } else { // If there is a suggested ID, use it if not already present. if ( ! empty( $import_id ) ) { $import_id = (int) $import_id; if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE ID = %d", $import_id ) ) ) { $data['ID'] = $import_id; } } if ( false === $wpdb->insert( $wpdb->posts, $data ) ) { if ( $wp_error ) { if ( 'attachment' === $post_type ) { $message = __( 'Could not insert attachment into the database.' ); } else { $message = __( 'Could not insert post into the database.' ); } return new WP_Error( 'db_insert_error', $message, $wpdb->last_error ); } else { return 0; } } $post_id = (int) $wpdb->insert_id; // Use the newly generated $post_id. $where = array( 'ID' => $post_id ); } if ( empty( $data['post_name'] ) && ! in_array( $data['post_status'], array( 'draft', 'pending', 'auto-draft' ), true ) ) { $data['post_name'] = wp_unique_post_slug( sanitize_title( $data['post_title'], $post_id ), $post_id, $data['post_status'], $post_type, $post_parent ); $wpdb->update( $wpdb->posts, array( 'post_name' => $data['post_name'] ), $where ); clean_post_cache( $post_id ); } if ( is_object_in_taxonomy( $post_type, 'category' ) ) { wp_set_post_categories( $post_id, $post_category ); } if ( isset( $postarr['tags_input'] ) && is_object_in_taxonomy( $post_type, 'post_tag' ) ) { wp_set_post_tags( $post_id, $postarr['tags_input'] ); } // Add default term for all associated custom taxonomies. if ( 'auto-draft' !== $post_status ) { foreach ( get_object_taxonomies( $post_type, 'object' ) as $taxonomy => $tax_object ) { if ( ! empty( $tax_object->default_term ) ) { // Filter out empty terms. if ( isset( $postarr['tax_input'][ $taxonomy ] ) && is_array( $postarr['tax_input'][ $taxonomy ] ) ) { $postarr['tax_input'][ $taxonomy ] = array_filter( $postarr['tax_input'][ $taxonomy ] ); } // Passed custom taxonomy list overwrites the existing list if not empty. $terms = wp_get_object_terms( $post_id, $taxonomy, array( 'fields' => 'ids' ) ); if ( ! empty( $terms ) && empty( $postarr['tax_input'][ $taxonomy ] ) ) { $postarr['tax_input'][ $taxonomy ] = $terms; } if ( empty( $postarr['tax_input'][ $taxonomy ] ) ) { $default_term_id = get_option( 'default_term_' . $taxonomy ); if ( ! empty( $default_term_id ) ) { $postarr['tax_input'][ $taxonomy ] = array( (int) $default_term_id ); } } } } } // New-style support for all custom taxonomies. if ( ! empty( $postarr['tax_input'] ) ) { foreach ( $postarr['tax_input'] as $taxonomy => $tags ) { $taxonomy_obj = get_taxonomy( $taxonomy ); if ( ! $taxonomy_obj ) { /* translators: %s: Taxonomy name. */ _doing_it_wrong( __FUNCTION__, sprintf( __( 'Invalid taxonomy: %s.' ), $taxonomy ), '4.4.0' ); continue; } // array = hierarchical, string = non-hierarchical. if ( is_array( $tags ) ) { $tags = array_filter( $tags ); } if ( current_user_can( $taxonomy_obj->cap->assign_terms ) ) { wp_set_post_terms( $post_id, $tags, $taxonomy ); } } } if ( ! empty( $postarr['meta_input'] ) ) { foreach ( $postarr['meta_input'] as $field => $value ) { update_post_meta( $post_id, $field, $value ); } } $current_guid = get_post_field( 'guid', $post_id ); // Set GUID. if ( ! $update && '' === $current_guid ) { $wpdb->update( $wpdb->posts, array( 'guid' => get_permalink( $post_id ) ), $where ); } if ( 'attachment' === $postarr['post_type'] ) { if ( ! empty( $postarr['file'] ) ) { update_attached_file( $post_id, $postarr['file'] ); } if ( ! empty( $postarr['context'] ) ) { add_post_meta( $post_id, '_wp_attachment_context', $postarr['context'], true ); } } // Set or remove featured image. if ( isset( $postarr['_thumbnail_id'] ) ) { $thumbnail_support = current_theme_supports( 'post-thumbnails', $post_type ) && post_type_supports( $post_type, 'thumbnail' ) || 'revision' === $post_type; if ( ! $thumbnail_support && 'attachment' === $post_type && $post_mime_type ) { if ( wp_attachment_is( 'audio', $post_id ) ) { $thumbnail_support = post_type_supports( 'attachment:audio', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:audio' ); } elseif ( wp_attachment_is( 'video', $post_id ) ) { $thumbnail_support = post_type_supports( 'attachment:video', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:video' ); } } if ( $thumbnail_support ) { $thumbnail_id = (int) $postarr['_thumbnail_id']; if ( -1 === $thumbnail_id ) { delete_post_thumbnail( $post_id ); } else { set_post_thumbnail( $post_id, $thumbnail_id ); } } } clean_post_cache( $post_id ); $post = get_post( $post_id ); if ( ! empty( $postarr['page_template'] ) ) { $post->page_template = $postarr['page_template']; $page_templates = wp_get_theme()->get_page_templates( $post ); if ( 'default' !== $postarr['page_template'] && ! isset( $page_templates[ $postarr['page_template'] ] ) ) { if ( $wp_error ) { return new WP_Error( 'invalid_page_template', __( 'Invalid page template.' ) ); } update_post_meta( $post_id, '_wp_page_template', 'default' ); } else { update_post_meta( $post_id, '_wp_page_template', $postarr['page_template'] ); } } if ( 'attachment' !== $postarr['post_type'] ) { wp_transition_post_status( $data['post_status'], $previous_status, $post ); } else { if ( $update ) { /** * Fires once an existing attachment has been updated. * * @since 2.0.0 * * @param int $post_id Attachment ID. */ do_action( 'edit_attachment', $post_id ); $post_after = get_post( $post_id ); /** * Fires once an existing attachment has been updated. * * @since 4.4.0 * * @param int $post_id Post ID. * @param WP_Post $post_after Post object following the update. * @param WP_Post $post_before Post object before the update. */ do_action( 'attachment_updated', $post_id, $post_after, $post_before ); } else { /** * Fires once an attachment has been added. * * @since 2.0.0 * * @param int $post_id Attachment ID. */ do_action( 'add_attachment', $post_id ); } return $post_id; } if ( $update ) { /** * Fires once an existing post has been updated. * * The dynamic portion of the hook name, `$post->post_type`, refers to * the post type slug. * * Possible hook names include: * * - `edit_post_post` * - `edit_post_page` * * @since 5.1.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. */ do_action( "edit_post_{$post->post_type}", $post_id, $post ); /** * Fires once an existing post has been updated. * * @since 1.2.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. */ do_action( 'edit_post', $post_id, $post ); $post_after = get_post( $post_id ); /** * Fires once an existing post has been updated. * * @since 3.0.0 * * @param int $post_id Post ID. * @param WP_Post $post_after Post object following the update. * @param WP_Post $post_before Post object before the update. */ do_action( 'post_updated', $post_id, $post_after, $post_before ); } /** * Fires once a post has been saved. * * The dynamic portion of the hook name, `$post->post_type`, refers to * the post type slug. * * Possible hook names include: * * - `save_post_post` * - `save_post_page` * * @since 3.7.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @param bool $update Whether this is an existing post being updated. */ do_action( "save_post_{$post->post_type}", $post_id, $post, $update ); /** * Fires once a post has been saved. * * @since 1.5.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @param bool $update Whether this is an existing post being updated. */ do_action( 'save_post', $post_id, $post, $update ); /** * Fires once a post has been saved. * * @since 2.0.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @param bool $update Whether this is an existing post being updated. */ do_action( 'wp_insert_post', $post_id, $post, $update ); if ( $fire_after_hooks ) { wp_after_insert_post( $post, $update, $post_before ); } return $post_id; } /** * Updates a post with new post data. * * The date does not have to be set for drafts. You can set the date and it will * not be overridden. * * @since 1.0.0 * @since 3.5.0 Added the `$wp_error` parameter to allow a WP_Error to be returned on failure. * @since 5.6.0 Added the `$fire_after_hooks` parameter. * * @param array|object $postarr Optional. Post data. Arrays are expected to be escaped, * objects are not. See wp_insert_post() for accepted arguments. * Default array. * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false. * @param bool $fire_after_hooks Optional. Whether to fire the after insert hooks. Default true. * @return int|WP_Error The post ID on success. The value 0 or WP_Error on failure. */ function wp_update_post( $postarr = array(), $wp_error = false, $fire_after_hooks = true ) { if ( is_object( $postarr ) ) { // Non-escaped post was passed. $postarr = get_object_vars( $postarr ); $postarr = wp_slash( $postarr ); } // First, get all of the original fields. $post = get_post( $postarr['ID'], ARRAY_A ); if ( is_null( $post ) ) { if ( $wp_error ) { return new WP_Error( 'invalid_post', __( 'Invalid post ID.' ) ); } return 0; } // Escape data pulled from DB. $post = wp_slash( $post ); // Passed post category list overwrites existing category list if not empty. if ( isset( $postarr['post_category'] ) && is_array( $postarr['post_category'] ) && count( $postarr['post_category'] ) > 0 ) { $post_cats = $postarr['post_category']; } else { $post_cats = $post['post_category']; } // Drafts shouldn't be assigned a date unless explicitly done so by the user. if ( isset( $post['post_status'] ) && in_array( $post['post_status'], array( 'draft', 'pending', 'auto-draft' ), true ) && empty( $postarr['edit_date'] ) && ( '0000-00-00 00:00:00' === $post['post_date_gmt'] ) ) { $clear_date = true; } else { $clear_date = false; } // Merge old and new fields with new fields overwriting old ones. $postarr = array_merge( $post, $postarr ); $postarr['post_category'] = $post_cats; if ( $clear_date ) { $postarr['post_date'] = current_time( 'mysql' ); $postarr['post_date_gmt'] = ''; } if ( 'attachment' === $postarr['post_type'] ) { return wp_insert_attachment( $postarr, false, 0, $wp_error ); } // Discard 'tags_input' parameter if it's the same as existing post tags. if ( isset( $postarr['tags_input'] ) && is_object_in_taxonomy( $postarr['post_type'], 'post_tag' ) ) { $tags = get_the_terms( $postarr['ID'], 'post_tag' ); $tag_names = array(); if ( $tags && ! is_wp_error( $tags ) ) { $tag_names = wp_list_pluck( $tags, 'name' ); } if ( $postarr['tags_input'] === $tag_names ) { unset( $postarr['tags_input'] ); } } return wp_insert_post( $postarr, $wp_error, $fire_after_hooks ); } /** * Publishes a post by transitioning the post status. * * @since 2.1.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int|WP_Post $post Post ID or post object. */ function wp_publish_post( $post ) { global $wpdb; $post = get_post( $post ); if ( ! $post ) { return; } if ( 'publish' === $post->post_status ) { return; } $post_before = get_post( $post->ID ); // Ensure at least one term is applied for taxonomies with a default term. foreach ( get_object_taxonomies( $post->post_type, 'object' ) as $taxonomy => $tax_object ) { // Skip taxonomy if no default term is set. if ( 'category' !== $taxonomy && empty( $tax_object->default_term ) ) { continue; } // Do not modify previously set terms. if ( ! empty( get_the_terms( $post, $taxonomy ) ) ) { continue; } if ( 'category' === $taxonomy ) { $default_term_id = (int) get_option( 'default_category', 0 ); } else { $default_term_id = (int) get_option( 'default_term_' . $taxonomy, 0 ); } if ( ! $default_term_id ) { continue; } wp_set_post_terms( $post->ID, array( $default_term_id ), $taxonomy ); } $wpdb->update( $wpdb->posts, array( 'post_status' => 'publish' ), array( 'ID' => $post->ID ) ); clean_post_cache( $post->ID ); $old_status = $post->post_status; $post->post_status = 'publish'; wp_transition_post_status( 'publish', $old_status, $post ); /** This action is documented in wp-includes/post.php */ do_action( "edit_post_{$post->post_type}", $post->ID, $post ); /** This action is documented in wp-includes/post.php */ do_action( 'edit_post', $post->ID, $post ); /** This action is documented in wp-includes/post.php */ do_action( "save_post_{$post->post_type}", $post->ID, $post, true ); /** This action is documented in wp-includes/post.php */ do_action( 'save_post', $post->ID, $post, true ); /** This action is documented in wp-includes/post.php */ do_action( 'wp_insert_post', $post->ID, $post, true ); wp_after_insert_post( $post, true, $post_before ); } /** * Publishes future post and make sure post ID has future post status. * * Invoked by cron 'publish_future_post' event. This safeguard prevents cron * from publishing drafts, etc. * * @since 2.5.0 * * @param int|WP_Post $post Post ID or post object. */ function check_and_publish_future_post( $post ) { $post = get_post( $post ); if ( ! $post ) { return; } if ( 'future' !== $post->post_status ) { return; } $time = strtotime( $post->post_date_gmt . ' GMT' ); // Uh oh, someone jumped the gun! if ( $time > time() ) { wp_clear_scheduled_hook( 'publish_future_post', array( $post->ID ) ); // Clear anything else in the system. wp_schedule_single_event( $time, 'publish_future_post', array( $post->ID ) ); return; } // wp_publish_post() returns no meaningful value. wp_publish_post( $post->ID ); } /** * Uses wp_checkdate to return a valid Gregorian-calendar value for post_date. * If post_date is not provided, this first checks post_date_gmt if provided, * then falls back to use the current time. * * For back-compat purposes in wp_insert_post, an empty post_date and an invalid * post_date_gmt will continue to return '1970-01-01 00:00:00' rather than false. * * @since 5.7.0 * * @param string $post_date The date in mysql format (`Y-m-d H:i:s`). * @param string $post_date_gmt The GMT date in mysql format (`Y-m-d H:i:s`). * @return string|false A valid Gregorian-calendar date string, or false on failure. */ function wp_resolve_post_date( $post_date = '', $post_date_gmt = '' ) { // If the date is empty, set the date to now. if ( empty( $post_date ) || '0000-00-00 00:00:00' === $post_date ) { if ( empty( $post_date_gmt ) || '0000-00-00 00:00:00' === $post_date_gmt ) { $post_date = current_time( 'mysql' ); } else { $post_date = get_date_from_gmt( $post_date_gmt ); } } // Validate the date. $month = (int) substr( $post_date, 5, 2 ); $day = (int) substr( $post_date, 8, 2 ); $year = (int) substr( $post_date, 0, 4 ); $valid_date = wp_checkdate( $month, $day, $year, $post_date ); if ( ! $valid_date ) { return false; } return $post_date; } /** * Computes a unique slug for the post, when given the desired slug and some post details. * * @since 2.8.0 * * @global wpdb $wpdb WordPress database abstraction object. * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param string $slug The desired slug (post_name). * @param int $post_id Post ID. * @param string $post_status No uniqueness checks are made if the post is still draft or pending. * @param string $post_type Post type. * @param int $post_parent Post parent ID. * @return string Unique slug for the post, based on $post_name (with a -1, -2, etc. suffix) */ function wp_unique_post_slug( $slug, $post_id, $post_status, $post_type, $post_parent ) { if ( in_array( $post_status, array( 'draft', 'pending', 'auto-draft' ), true ) || ( 'inherit' === $post_status && 'revision' === $post_type ) || 'user_request' === $post_type ) { return $slug; } /** * Filters the post slug before it is generated to be unique. * * Returning a non-null value will short-circuit the * unique slug generation, returning the passed value instead. * * @since 5.1.0 * * @param string|null $override_slug Short-circuit return value. * @param string $slug The desired slug (post_name). * @param int $post_id Post ID. * @param string $post_status The post status. * @param string $post_type Post type. * @param int $post_parent Post parent ID. */ $override_slug = apply_filters( 'pre_wp_unique_post_slug', null, $slug, $post_id, $post_status, $post_type, $post_parent ); if ( null !== $override_slug ) { return $override_slug; } global $wpdb, $wp_rewrite; $original_slug = $slug; $feeds = $wp_rewrite->feeds; if ( ! is_array( $feeds ) ) { $feeds = array(); } if ( 'attachment' === $post_type ) { // Attachment slugs must be unique across all types. $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND ID != %d LIMIT 1"; $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $slug, $post_id ) ); /** * Filters whether the post slug would make a bad attachment slug. * * @since 3.1.0 * * @param bool $bad_slug Whether the slug would be bad as an attachment slug. * @param string $slug The post slug. */ $is_bad_attachment_slug = apply_filters( 'wp_unique_post_slug_is_bad_attachment_slug', false, $slug ); if ( $post_name_check || in_array( $slug, $feeds, true ) || 'embed' === $slug || $is_bad_attachment_slug ) { $suffix = 2; do { $alt_post_name = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix"; $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $alt_post_name, $post_id ) ); ++$suffix; } while ( $post_name_check ); $slug = $alt_post_name; } } elseif ( is_post_type_hierarchical( $post_type ) ) { if ( 'nav_menu_item' === $post_type ) { return $slug; } /* * Page slugs must be unique within their own trees. Pages are in a separate * namespace than posts so page slugs are allowed to overlap post slugs. */ $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND post_type IN ( %s, 'attachment' ) AND ID != %d AND post_parent = %d LIMIT 1"; $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $slug, $post_type, $post_id, $post_parent ) ); /** * Filters whether the post slug would make a bad hierarchical post slug. * * @since 3.1.0 * * @param bool $bad_slug Whether the post slug would be bad in a hierarchical post context. * @param string $slug The post slug. * @param string $post_type Post type. * @param int $post_parent Post parent ID. */ $is_bad_hierarchical_slug = apply_filters( 'wp_unique_post_slug_is_bad_hierarchical_slug', false, $slug, $post_type, $post_parent ); if ( $post_name_check || in_array( $slug, $feeds, true ) || 'embed' === $slug || preg_match( "@^($wp_rewrite->pagination_base)?\d+$@", $slug ) || $is_bad_hierarchical_slug ) { $suffix = 2; do { $alt_post_name = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix"; $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $alt_post_name, $post_type, $post_id, $post_parent ) ); ++$suffix; } while ( $post_name_check ); $slug = $alt_post_name; } } else { // Post slugs must be unique across all posts. $check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND post_type = %s AND ID != %d LIMIT 1"; $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $slug, $post_type, $post_id ) ); $post = get_post( $post_id ); // Prevent new post slugs that could result in URLs that conflict with date archives. $conflicts_with_date_archive = false; if ( 'post' === $post_type && ( ! $post || $post->post_name !== $slug ) && preg_match( '/^[0-9]+$/', $slug ) ) { $slug_num = (int) $slug; if ( $slug_num ) { $permastructs = array_values( array_filter( explode( '/', get_option( 'permalink_structure' ) ) ) ); $postname_index = array_search( '%postname%', $permastructs, true ); /* * Potential date clashes are as follows: * * - Any integer in the first permastruct position could be a year. * - An integer between 1 and 12 that follows 'year' conflicts with 'monthnum'. * - An integer between 1 and 31 that follows 'monthnum' conflicts with 'day'. */ if ( 0 === $postname_index || ( $postname_index && '%year%' === $permastructs[ $postname_index - 1 ] && 13 > $slug_num ) || ( $postname_index && '%monthnum%' === $permastructs[ $postname_index - 1 ] && 32 > $slug_num ) ) { $conflicts_with_date_archive = true; } } } /** * Filters whether the post slug would be bad as a flat slug. * * @since 3.1.0 * * @param bool $bad_slug Whether the post slug would be bad as a flat slug. * @param string $slug The post slug. * @param string $post_type Post type. */ $is_bad_flat_slug = apply_filters( 'wp_unique_post_slug_is_bad_flat_slug', false, $slug, $post_type ); if ( $post_name_check || in_array( $slug, $feeds, true ) || 'embed' === $slug || $conflicts_with_date_archive || $is_bad_flat_slug ) { $suffix = 2; do { $alt_post_name = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix"; $post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $alt_post_name, $post_type, $post_id ) ); ++$suffix; } while ( $post_name_check ); $slug = $alt_post_name; } } /** * Filters the unique post slug. * * @since 3.3.0 * * @param string $slug The post slug. * @param int $post_id Post ID. * @param string $post_status The post status. * @param string $post_type Post type. * @param int $post_parent Post parent ID * @param string $original_slug The original post slug. */ return apply_filters( 'wp_unique_post_slug', $slug, $post_id, $post_status, $post_type, $post_parent, $original_slug ); } /** * Truncates a post slug. * * @since 3.6.0 * @access private * * @see utf8_uri_encode() * * @param string $slug The slug to truncate. * @param int $length Optional. Max length of the slug. Default 200 (characters). * @return string The truncated slug. */ function _truncate_post_slug( $slug, $length = 200 ) { if ( strlen( $slug ) > $length ) { $decoded_slug = urldecode( $slug ); if ( $decoded_slug === $slug ) { $slug = substr( $slug, 0, $length ); } else { $slug = utf8_uri_encode( $decoded_slug, $length, true ); } } return rtrim( $slug, '-' ); } /** * Adds tags to a post. * * @see wp_set_post_tags() * * @since 2.3.0 * * @param int $post_id Optional. The Post ID. Does not default to the ID of the global $post. * @param string|array $tags Optional. An array of tags to set for the post, or a string of tags * separated by commas. Default empty. * @return array|false|WP_Error Array of affected term IDs. WP_Error or false on failure. */ function wp_add_post_tags( $post_id = 0, $tags = '' ) { return wp_set_post_tags( $post_id, $tags, true ); } /** * Sets the tags for a post. * * @since 2.3.0 * * @see wp_set_object_terms() * * @param int $post_id Optional. The Post ID. Does not default to the ID of the global $post. * @param string|array $tags Optional. An array of tags to set for the post, or a string of tags * separated by commas. Default empty. * @param bool $append Optional. If true, don't delete existing tags, just add on. If false, * replace the tags with the new tags. Default false. * @return array|false|WP_Error Array of term taxonomy IDs of affected terms. WP_Error or false on failure. */ function wp_set_post_tags( $post_id = 0, $tags = '', $append = false ) { return wp_set_post_terms( $post_id, $tags, 'post_tag', $append ); } /** * Sets the terms for a post. * * @since 2.8.0 * * @see wp_set_object_terms() * * @param int $post_id Optional. The Post ID. Does not default to the ID of the global $post. * @param string|array $terms Optional. An array of terms to set for the post, or a string of terms * separated by commas. Hierarchical taxonomies must always pass IDs rather * than names so that children with the same names but different parents * aren't confused. Default empty. * @param string $taxonomy Optional. Taxonomy name. Default 'post_tag'. * @param bool $append Optional. If true, don't delete existing terms, just add on. If false, * replace the terms with the new terms. Default false. * @return array|false|WP_Error Array of term taxonomy IDs of affected terms. WP_Error or false on failure. */ function wp_set_post_terms( $post_id = 0, $terms = '', $taxonomy = 'post_tag', $append = false ) { $post_id = (int) $post_id; if ( ! $post_id ) { return false; } if ( empty( $terms ) ) { $terms = array(); } if ( ! is_array( $terms ) ) { $comma = _x( ',', 'tag delimiter' ); if ( ',' !== $comma ) { $terms = str_replace( $comma, ',', $terms ); } $terms = explode( ',', trim( $terms, " \n\t\r\0\x0B," ) ); } /* * Hierarchical taxonomies must always pass IDs rather than names so that * children with the same names but different parents aren't confused. */ if ( is_taxonomy_hierarchical( $taxonomy ) ) { $terms = array_unique( array_map( 'intval', $terms ) ); } return wp_set_object_terms( $post_id, $terms, $taxonomy, $append ); } /** * Sets categories for a post. * * If no categories are provided, the default category is used. * * @since 2.1.0 * * @param int $post_id Optional. The Post ID. Does not default to the ID * of the global $post. Default 0. * @param int[]|int $post_categories Optional. List of category IDs, or the ID of a single category. * Default empty array. * @param bool $append If true, don't delete existing categories, just add on. * If false, replace the categories with the new categories. * @return array|false|WP_Error Array of term taxonomy IDs of affected categories. WP_Error or false on failure. */ function wp_set_post_categories( $post_id = 0, $post_categories = array(), $append = false ) { $post_id = (int) $post_id; $post_type = get_post_type( $post_id ); $post_status = get_post_status( $post_id ); // If $post_categories isn't already an array, make it one. $post_categories = (array) $post_categories; if ( empty( $post_categories ) ) { /** * Filters post types (in addition to 'post') that require a default category. * * @since 5.5.0 * * @param string[] $post_types An array of post type names. Default empty array. */ $default_category_post_types = apply_filters( 'default_category_post_types', array() ); // Regular posts always require a default category. $default_category_post_types = array_merge( $default_category_post_types, array( 'post' ) ); if ( in_array( $post_type, $default_category_post_types, true ) && is_object_in_taxonomy( $post_type, 'category' ) && 'auto-draft' !== $post_status ) { $post_categories = array( get_option( 'default_category' ) ); $append = false; } else { $post_categories = array(); } } elseif ( 1 === count( $post_categories ) && '' === reset( $post_categories ) ) { return true; } return wp_set_post_terms( $post_id, $post_categories, 'category', $append ); } /** * Fires actions related to the transitioning of a post's status. * * When a post is saved, the post status is "transitioned" from one status to another, * though this does not always mean the status has actually changed before and after * the save. This function fires a number of action hooks related to that transition: * the generic {@see 'transition_post_status'} action, as well as the dynamic hooks * {@see '$old_status_to_$new_status'} and {@see '$new_status_$post->post_type'}. Note * that the function does not transition the post object in the database. * * For instance: When publishing a post for the first time, the post status may transition * from 'draft' – or some other status – to 'publish'. However, if a post is already * published and is simply being updated, the "old" and "new" statuses may both be 'publish' * before and after the transition. * * @since 2.3.0 * * @param string $new_status Transition to this post status. * @param string $old_status Previous post status. * @param WP_Post $post Post data. */ function wp_transition_post_status( $new_status, $old_status, $post ) { /** * Fires when a post is transitioned from one status to another. * * @since 2.3.0 * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. */ do_action( 'transition_post_status', $new_status, $old_status, $post ); /** * Fires when a post is transitioned from one status to another. * * The dynamic portions of the hook name, `$new_status` and `$old_status`, * refer to the old and new post statuses, respectively. * * Possible hook names include: * * - `draft_to_publish` * - `publish_to_trash` * - `pending_to_draft` * * @since 2.3.0 * * @param WP_Post $post Post object. */ do_action( "{$old_status}_to_{$new_status}", $post ); /** * Fires when a post is transitioned from one status to another. * * The dynamic portions of the hook name, `$new_status` and `$post->post_type`, * refer to the new post status and post type, respectively. * * Possible hook names include: * * - `draft_post` * - `future_post` * - `pending_post` * - `private_post` * - `publish_post` * - `trash_post` * - `draft_page` * - `future_page` * - `pending_page` * - `private_page` * - `publish_page` * - `trash_page` * - `publish_attachment` * - `trash_attachment` * * Please note: When this action is hooked using a particular post status (like * 'publish', as `publish_{$post->post_type}`), it will fire both when a post is * first transitioned to that status from something else, as well as upon * subsequent post updates (old and new status are both the same). * * Therefore, if you are looking to only fire a callback when a post is first * transitioned to a status, use the {@see 'transition_post_status'} hook instead. * * @since 2.3.0 * @since 5.9.0 Added `$old_status` parameter. * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @param string $old_status Old post status. */ do_action( "{$new_status}_{$post->post_type}", $post->ID, $post, $old_status ); } /** * Fires actions after a post, its terms and meta data has been saved. * * @since 5.6.0 * * @param int|WP_Post $post The post ID or object that has been saved. * @param bool $update Whether this is an existing post being updated. * @param null|WP_Post $post_before Null for new posts, the WP_Post object prior * to the update for updated posts. */ function wp_after_insert_post( $post, $update, $post_before ) { $post = get_post( $post ); if ( ! $post ) { return; } $post_id = $post->ID; /** * Fires once a post, its terms and meta data has been saved. * * @since 5.6.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. * @param bool $update Whether this is an existing post being updated. * @param null|WP_Post $post_before Null for new posts, the WP_Post object prior * to the update for updated posts. */ do_action( 'wp_after_insert_post', $post_id, $post, $update, $post_before ); } // // Comment, trackback, and pingback functions. // /** * Adds a URL to those already pinged. * * @since 1.5.0 * @since 4.7.0 `$post` can be a WP_Post object. * @since 4.7.0 `$uri` can be an array of URIs. * * @global wpdb $wpdb WordPress database abstraction object. * * @param int|WP_Post $post Post ID or post object. * @param string|array $uri Ping URI or array of URIs. * @return int|false How many rows were updated. */ function add_ping( $post, $uri ) { global $wpdb; $post = get_post( $post ); if ( ! $post ) { return false; } $pung = trim( $post->pinged ); $pung = preg_split( '/\s/', $pung ); if ( is_array( $uri ) ) { $pung = array_merge( $pung, $uri ); } else { $pung[] = $uri; } $new = implode( "\n", $pung ); /** * Filters the new ping URL to add for the given post. * * @since 2.0.0 * * @param string $new New ping URL to add. */ $new = apply_filters( 'add_ping', $new ); $return = $wpdb->update( $wpdb->posts, array( 'pinged' => $new ), array( 'ID' => $post->ID ) ); clean_post_cache( $post->ID ); return $return; } /** * Retrieves enclosures already enclosed for a post. * * @since 1.5.0 * * @param int $post_id Post ID. * @return string[] Array of enclosures for the given post. */ function get_enclosed( $post_id ) { $custom_fields = get_post_custom( $post_id ); $pung = array(); if ( ! is_array( $custom_fields ) ) { return $pung; } foreach ( $custom_fields as $key => $val ) { if ( 'enclosure' !== $key || ! is_array( $val ) ) { continue; } foreach ( $val as $enc ) { $enclosure = explode( "\n", $enc ); $pung[] = trim( $enclosure[0] ); } } /** * Filters the list of enclosures already enclosed for the given post. * * @since 2.0.0 * * @param string[] $pung Array of enclosures for the given post. * @param int $post_id Post ID. */ return apply_filters( 'get_enclosed', $pung, $post_id ); } /** * Retrieves URLs already pinged for a post. * * @since 1.5.0 * * @since 4.7.0 `$post` can be a WP_Post object. * * @param int|WP_Post $post Post ID or object. * @return string[]|false Array of URLs already pinged for the given post, false if the post is not found. */ function get_pung( $post ) { $post = get_post( $post ); if ( ! $post ) { return false; } $pung = trim( $post->pinged ); $pung = preg_split( '/\s/', $pung ); /** * Filters the list of already-pinged URLs for the given post. * * @since 2.0.0 * * @param string[] $pung Array of URLs already pinged for the given post. */ return apply_filters( 'get_pung', $pung ); } /** * Retrieves URLs that need to be pinged. * * @since 1.5.0 * @since 4.7.0 `$post` can be a WP_Post object. * * @param int|WP_Post $post Post ID or post object. * @return string[]|false List of URLs yet to ping. */ function get_to_ping( $post ) { $post = get_post( $post ); if ( ! $post ) { return false; } $to_ping = sanitize_trackback_urls( $post->to_ping ); $to_ping = preg_split( '/\s/', $to_ping, -1, PREG_SPLIT_NO_EMPTY ); /** * Filters the list of URLs yet to ping for the given post. * * @since 2.0.0 * * @param string[] $to_ping List of URLs yet to ping. */ return apply_filters( 'get_to_ping', $to_ping ); } /** * Does trackbacks for a list of URLs. * * @since 1.0.0 * * @param string $tb_list Comma separated list of URLs. * @param int $post_id Post ID. */ function trackback_url_list( $tb_list, $post_id ) { if ( ! empty( $tb_list ) ) { // Get post data. $postdata = get_post( $post_id, ARRAY_A ); // Form an excerpt. $excerpt = strip_tags( $postdata['post_excerpt'] ? $postdata['post_excerpt'] : $postdata['post_content'] ); if ( strlen( $excerpt ) > 255 ) { $excerpt = substr( $excerpt, 0, 252 ) . '…'; } $trackback_urls = explode( ',', $tb_list ); foreach ( (array) $trackback_urls as $tb_url ) { $tb_url = trim( $tb_url ); trackback( $tb_url, wp_unslash( $postdata['post_title'] ), $excerpt, $post_id ); } } } // // Page functions. // /** * Gets a list of page IDs. * * @since 2.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @return string[] List of page IDs as strings. */ function get_all_page_ids() { global $wpdb; $page_ids = wp_cache_get( 'all_page_ids', 'posts' ); if ( ! is_array( $page_ids ) ) { $page_ids = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_type = 'page'" ); wp_cache_add( 'all_page_ids', $page_ids, 'posts' ); } return $page_ids; } /** * Retrieves page data given a page ID or page object. * * Use get_post() instead of get_page(). * * @since 1.5.1 * @deprecated 3.5.0 Use get_post() * * @param int|WP_Post $page Page object or page ID. Passed by reference. * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which * correspond to a WP_Post object, an associative array, or a numeric array, * respectively. Default OBJECT. * @param string $filter Optional. How the return value should be filtered. Accepts 'raw', * 'edit', 'db', 'display'. Default 'raw'. * @return WP_Post|array|null WP_Post or array on success, null on failure. */ function get_page( $page, $output = OBJECT, $filter = 'raw' ) { return get_post( $page, $output, $filter ); } /** * Retrieves a page given its path. * * @since 2.1.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $page_path Page path. * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which * correspond to a WP_Post object, an associative array, or a numeric array, * respectively. Default OBJECT. * @param string|array $post_type Optional. Post type or array of post types. Default 'page'. * @return WP_Post|array|null WP_Post (or array) on success, or null on failure. */ function get_page_by_path( $page_path, $output = OBJECT, $post_type = 'page' ) { global $wpdb; $last_changed = wp_cache_get_last_changed( 'posts' ); $hash = md5( $page_path . serialize( $post_type ) ); $cache_key = "get_page_by_path:$hash:$last_changed"; $cached = wp_cache_get( $cache_key, 'post-queries' ); if ( false !== $cached ) { // Special case: '0' is a bad `$page_path`. if ( '0' === $cached || 0 === $cached ) { return; } else { return get_post( $cached, $output ); } } $page_path = rawurlencode( urldecode( $page_path ) ); $page_path = str_replace( '%2F', '/', $page_path ); $page_path = str_replace( '%20', ' ', $page_path ); $parts = explode( '/', trim( $page_path, '/' ) ); $parts = array_map( 'sanitize_title_for_query', $parts ); $escaped_parts = esc_sql( $parts ); $in_string = "'" . implode( "','", $escaped_parts ) . "'"; if ( is_array( $post_type ) ) { $post_types = $post_type; } else { $post_types = array( $post_type, 'attachment' ); } $post_types = esc_sql( $post_types ); $post_type_in_string = "'" . implode( "','", $post_types ) . "'"; $sql = " SELECT ID, post_name, post_parent, post_type FROM $wpdb->posts WHERE post_name IN ($in_string) AND post_type IN ($post_type_in_string) "; $pages = $wpdb->get_results( $sql, OBJECT_K ); $revparts = array_reverse( $parts ); $foundid = 0; foreach ( (array) $pages as $page ) { if ( $page->post_name == $revparts[0] ) { $count = 0; $p = $page; /* * Loop through the given path parts from right to left, * ensuring each matches the post ancestry. */ while ( 0 != $p->post_parent && isset( $pages[ $p->post_parent ] ) ) { ++$count; $parent = $pages[ $p->post_parent ]; if ( ! isset( $revparts[ $count ] ) || $parent->post_name != $revparts[ $count ] ) { break; } $p = $parent; } if ( 0 == $p->post_parent && count( $revparts ) === $count + 1 && $p->post_name == $revparts[ $count ] ) { $foundid = $page->ID; if ( $page->post_type == $post_type ) { break; } } } } // We cache misses as well as hits. wp_cache_set( $cache_key, $foundid, 'post-queries' ); if ( $foundid ) { return get_post( $foundid, $output ); } return null; } /** * Identifies descendants of a given page ID in a list of page objects. * * Descendants are identified from the `$pages` array passed to the function. No database queries are performed. * * @since 1.5.1 * * @param int $page_id Page ID. * @param WP_Post[] $pages List of page objects from which descendants should be identified. * @return WP_Post[] List of page children. */ function get_page_children( $page_id, $pages ) { // Build a hash of ID -> children. $children = array(); foreach ( (array) $pages as $page ) { $children[ (int) $page->post_parent ][] = $page; } $page_list = array(); // Start the search by looking at immediate children. if ( isset( $children[ $page_id ] ) ) { // Always start at the end of the stack in order to preserve original `$pages` order. $to_look = array_reverse( $children[ $page_id ] ); while ( $to_look ) { $p = array_pop( $to_look ); $page_list[] = $p; if ( isset( $children[ $p->ID ] ) ) { foreach ( array_reverse( $children[ $p->ID ] ) as $child ) { // Append to the `$to_look` stack to descend the tree. $to_look[] = $child; } } } } return $page_list; } /** * Orders the pages with children under parents in a flat list. * * It uses auxiliary structure to hold parent-children relationships and * runs in O(N) complexity * * @since 2.0.0 * * @param WP_Post[] $pages Posts array (passed by reference). * @param int $page_id Optional. Parent page ID. Default 0. * @return string[] Array of post names keyed by ID and arranged by hierarchy. Children immediately follow their parents. */ function get_page_hierarchy( &$pages, $page_id = 0 ) { if ( empty( $pages ) ) { return array(); } $children = array(); foreach ( (array) $pages as $p ) { $parent_id = (int) $p->post_parent; $children[ $parent_id ][] = $p; } $result = array(); _page_traverse_name( $page_id, $children, $result ); return $result; } /** * Traverses and return all the nested children post names of a root page. * * $children contains parent-children relations * * @since 2.9.0 * @access private * * @see _page_traverse_name() * * @param int $page_id Page ID. * @param array $children Parent-children relations (passed by reference). * @param string[] $result Array of page names keyed by ID (passed by reference). */ function _page_traverse_name( $page_id, &$children, &$result ) { if ( isset( $children[ $page_id ] ) ) { foreach ( (array) $children[ $page_id ] as $child ) { $result[ $child->ID ] = $child->post_name; _page_traverse_name( $child->ID, $children, $result ); } } } /** * Builds the URI path for a page. * * Sub pages will be in the "directory" under the parent page post name. * * @since 1.5.0 * @since 4.6.0 The `$page` parameter was made optional. * * @param WP_Post|object|int $page Optional. Page ID or WP_Post object. Default is global $post. * @return string|false Page URI, false on error. */ function get_page_uri( $page = 0 ) { if ( ! $page instanceof WP_Post ) { $page = get_post( $page ); } if ( ! $page ) { return false; } $uri = $page->post_name; foreach ( $page->ancestors as $parent ) { $parent = get_post( $parent ); if ( $parent && $parent->post_name ) { $uri = $parent->post_name . '/' . $uri; } } /** * Filters the URI for a page. * * @since 4.4.0 * * @param string $uri Page URI. * @param WP_Post $page Page object. */ return apply_filters( 'get_page_uri', $uri, $page ); } /** * Retrieves an array of pages (or hierarchical post type items). * * @since 1.5.0 * @since 6.3.0 Use WP_Query internally. * * @param array|string $args { * Optional. Array or string of arguments to retrieve pages. * * @type int $child_of Page ID to return child and grandchild pages of. Note: The value * of `$hierarchical` has no bearing on whether `$child_of` returns * hierarchical results. Default 0, or no restriction. * @type string $sort_order How to sort retrieved pages. Accepts 'ASC', 'DESC'. Default 'ASC'. * @type string $sort_column What columns to sort pages by, comma-separated. Accepts 'post_author', * 'post_date', 'post_title', 'post_name', 'post_modified', 'menu_order', * 'post_modified_gmt', 'post_parent', 'ID', 'rand', 'comment_count'. * 'post_' can be omitted for any values that start with it. * Default 'post_title'. * @type bool $hierarchical Whether to return pages hierarchically. If false in conjunction with * `$child_of` also being false, both arguments will be disregarded. * Default true. * @type int[] $exclude Array of page IDs to exclude. Default empty array. * @type int[] $include Array of page IDs to include. Cannot be used with `$child_of`, * `$parent`, `$exclude`, `$meta_key`, `$meta_value`, or `$hierarchical`. * Default empty array. * @type string $meta_key Only include pages with this meta key. Default empty. * @type string $meta_value Only include pages with this meta value. Requires `$meta_key`. * Default empty. * @type string $authors A comma-separated list of author IDs. Default empty. * @type int $parent Page ID to return direct children of. Default -1, or no restriction. * @type string|int[] $exclude_tree Comma-separated string or array of page IDs to exclude. * Default empty array. * @type int $number The number of pages to return. Default 0, or all pages. * @type int $offset The number of pages to skip before returning. Requires `$number`. * Default 0. * @type string $post_type The post type to query. Default 'page'. * @type string|array $post_status A comma-separated list or array of post statuses to include. * Default 'publish'. * } * @return WP_Post[]|false Array of pages (or hierarchical post type items). Boolean false if the * specified post type is not hierarchical or the specified status is not * supported by the post type. */ function get_pages( $args = array() ) { $defaults = array( 'child_of' => 0, 'sort_order' => 'ASC', 'sort_column' => 'post_title', 'hierarchical' => 1, 'exclude' => array(), 'include' => array(), 'meta_key' => '', 'meta_value' => '', 'authors' => '', 'parent' => -1, 'exclude_tree' => array(), 'number' => '', 'offset' => 0, 'post_type' => 'page', 'post_status' => 'publish', ); $parsed_args = wp_parse_args( $args, $defaults ); $number = (int) $parsed_args['number']; $offset = (int) $parsed_args['offset']; $child_of = (int) $parsed_args['child_of']; $hierarchical = $parsed_args['hierarchical']; $exclude = $parsed_args['exclude']; $meta_key = $parsed_args['meta_key']; $meta_value = $parsed_args['meta_value']; $parent = $parsed_args['parent']; $post_status = $parsed_args['post_status']; // Make sure the post type is hierarchical. $hierarchical_post_types = get_post_types( array( 'hierarchical' => true ) ); if ( ! in_array( $parsed_args['post_type'], $hierarchical_post_types, true ) ) { return false; } if ( $parent > 0 && ! $child_of ) { $hierarchical = false; } // Make sure we have a valid post status. if ( ! is_array( $post_status ) ) { $post_status = explode( ',', $post_status ); } if ( array_diff( $post_status, get_post_stati() ) ) { return false; } $query_args = array( 'orderby' => 'post_title', 'order' => 'ASC', 'post__not_in' => wp_parse_id_list( $exclude ), 'meta_key' => $meta_key, 'meta_value' => $meta_value, 'posts_per_page' => -1, 'offset' => $offset, 'post_type' => $parsed_args['post_type'], 'post_status' => $post_status, 'update_post_term_cache' => false, 'update_post_meta_cache' => false, 'ignore_sticky_posts' => true, 'no_found_rows' => true, ); if ( ! empty( $parsed_args['include'] ) ) { $child_of = 0; // Ignore child_of, parent, exclude, meta_key, and meta_value params if using include. $parent = -1; unset( $query_args['post__not_in'], $query_args['meta_key'], $query_args['meta_value'] ); $hierarchical = false; $query_args['post__in'] = wp_parse_id_list( $parsed_args['include'] ); } if ( ! empty( $parsed_args['authors'] ) ) { $post_authors = wp_parse_list( $parsed_args['authors'] ); if ( ! empty( $post_authors ) ) { $query_args['author__in'] = array(); foreach ( $post_authors as $post_author ) { // Do we have an author id or an author login? if ( 0 == (int) $post_author ) { $post_author = get_user_by( 'login', $post_author ); if ( empty( $post_author ) ) { continue; } if ( empty( $post_author->ID ) ) { continue; } $post_author = $post_author->ID; } $query_args['author__in'][] = (int) $post_author; } } } if ( is_array( $parent ) ) { $post_parent__in = array_map( 'absint', (array) $parent ); if ( ! empty( $post_parent__in ) ) { $query_args['post_parent__in'] = $post_parent__in; } } elseif ( $parent >= 0 ) { $query_args['post_parent'] = $parent; } /* * Maintain backward compatibility for `sort_column` key. * Additionally to `WP_Query`, it has been supporting the `post_modified_gmt` field, so this logic will translate * it to `post_modified` which should result in the same order given the two dates in the fields match. */ $orderby = wp_parse_list( $parsed_args['sort_column'] ); $orderby = array_map( static function ( $orderby_field ) { $orderby_field = trim( $orderby_field ); if ( 'post_modified_gmt' === $orderby_field || 'modified_gmt' === $orderby_field ) { $orderby_field = str_replace( '_gmt', '', $orderby_field ); } return $orderby_field; }, $orderby ); if ( $orderby ) { $query_args['orderby'] = array_fill_keys( $orderby, $parsed_args['sort_order'] ); } $order = $parsed_args['sort_order']; if ( $order ) { $query_args['order'] = $order; } if ( ! empty( $number ) ) { $query_args['posts_per_page'] = $number; } /** * Filters query arguments passed to WP_Query in get_pages. * * @since 6.3.0 * * @param array $query_args Array of arguments passed to WP_Query. * @param array $parsed_args Array of get_pages() arguments. */ $query_args = apply_filters( 'get_pages_query_args', $query_args, $parsed_args ); $pages = new WP_Query(); $pages = $pages->query( $query_args ); if ( $child_of || $hierarchical ) { $pages = get_page_children( $child_of, $pages ); } if ( ! empty( $parsed_args['exclude_tree'] ) ) { $exclude = wp_parse_id_list( $parsed_args['exclude_tree'] ); foreach ( $exclude as $id ) { $children = get_page_children( $id, $pages ); foreach ( $children as $child ) { $exclude[] = $child->ID; } } $num_pages = count( $pages ); for ( $i = 0; $i < $num_pages; $i++ ) { if ( in_array( $pages[ $i ]->ID, $exclude, true ) ) { unset( $pages[ $i ] ); } } } /** * Filters the retrieved list of pages. * * @since 2.1.0 * * @param WP_Post[] $pages Array of page objects. * @param array $parsed_args Array of get_pages() arguments. */ return apply_filters( 'get_pages', $pages, $parsed_args ); } // // Attachment functions. // /** * Determines whether an attachment URI is local and really an attachment. * * For more information on this and similar theme functions, check out * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ * Conditional Tags} article in the Theme Developer Handbook. * * @since 2.0.0 * * @param string $url URL to check * @return bool True on success, false on failure. */ function is_local_attachment( $url ) { if ( ! str_contains( $url, home_url() ) ) { return false; } if ( str_contains( $url, home_url( '/?attachment_id=' ) ) ) { return true; } $id = url_to_postid( $url ); if ( $id ) { $post = get_post( $id ); if ( 'attachment' === $post->post_type ) { return true; } } return false; } /** * Inserts an attachment. * * If you set the 'ID' in the $args parameter, it will mean that you are * updating and attempt to update the attachment. You can also set the * attachment name or title by setting the key 'post_name' or 'post_title'. * * You can set the dates for the attachment manually by setting the 'post_date' * and 'post_date_gmt' keys' values. * * By default, the comments will use the default settings for whether the * comments are allowed. You can close them manually or keep them open by * setting the value for the 'comment_status' key. * * @since 2.0.0 * @since 4.7.0 Added the `$wp_error` parameter to allow a WP_Error to be returned on failure. * @since 5.6.0 Added the `$fire_after_hooks` parameter. * * @see wp_insert_post() * * @param string|array $args Arguments for inserting an attachment. * @param string|false $file Optional. Filename. Default false. * @param int $parent_post_id Optional. Parent post ID or 0 for no parent. Default 0. * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false. * @param bool $fire_after_hooks Optional. Whether to fire the after insert hooks. Default true. * @return int|WP_Error The attachment ID on success. The value 0 or WP_Error on failure. */ function wp_insert_attachment( $args, $file = false, $parent_post_id = 0, $wp_error = false, $fire_after_hooks = true ) { $defaults = array( 'file' => $file, 'post_parent' => 0, ); $data = wp_parse_args( $args, $defaults ); if ( ! empty( $parent_post_id ) ) { $data['post_parent'] = $parent_post_id; } $data['post_type'] = 'attachment'; return wp_insert_post( $data, $wp_error, $fire_after_hooks ); } /** * Trashes or deletes an attachment. * * When an attachment is permanently deleted, the file will also be removed. * Deletion removes all post meta fields, taxonomy, comments, etc. associated * with the attachment (except the main post). * * The attachment is moved to the Trash instead of permanently deleted unless Trash * for media is disabled, item is already in the Trash, or $force_delete is true. * * @since 2.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $post_id Attachment ID. * @param bool $force_delete Optional. Whether to bypass Trash and force deletion. * Default false. * @return WP_Post|false|null Post data on success, false or null on failure. */ function wp_delete_attachment( $post_id, $force_delete = false ) { global $wpdb; $post = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id ) ); if ( ! $post ) { return $post; } $post = get_post( $post ); if ( 'attachment' !== $post->post_type ) { return false; } if ( ! $force_delete && EMPTY_TRASH_DAYS && MEDIA_TRASH && 'trash' !== $post->post_status ) { return wp_trash_post( $post_id ); } /** * Filters whether an attachment deletion should take place. * * @since 5.5.0 * * @param WP_Post|false|null $delete Whether to go forward with deletion. * @param WP_Post $post Post object. * @param bool $force_delete Whether to bypass the Trash. */ $check = apply_filters( 'pre_delete_attachment', null, $post, $force_delete ); if ( null !== $check ) { return $check; } delete_post_meta( $post_id, '_wp_trash_meta_status' ); delete_post_meta( $post_id, '_wp_trash_meta_time' ); $meta = wp_get_attachment_metadata( $post_id ); $backup_sizes = get_post_meta( $post->ID, '_wp_attachment_backup_sizes', true ); $file = get_attached_file( $post_id ); if ( is_multisite() && is_string( $file ) && ! empty( $file ) ) { clean_dirsize_cache( $file ); } /** * Fires before an attachment is deleted, at the start of wp_delete_attachment(). * * @since 2.0.0 * @since 5.5.0 Added the `$post` parameter. * * @param int $post_id Attachment ID. * @param WP_Post $post Post object. */ do_action( 'delete_attachment', $post_id, $post ); wp_delete_object_term_relationships( $post_id, array( 'category', 'post_tag' ) ); wp_delete_object_term_relationships( $post_id, get_object_taxonomies( $post->post_type ) ); // Delete all for any posts. delete_metadata( 'post', null, '_thumbnail_id', $post_id, true ); wp_defer_comment_counting( true ); $comment_ids = $wpdb->get_col( $wpdb->prepare( "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = %d ORDER BY comment_ID DESC", $post_id ) ); foreach ( $comment_ids as $comment_id ) { wp_delete_comment( $comment_id, true ); } wp_defer_comment_counting( false ); $post_meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE post_id = %d ", $post_id ) ); foreach ( $post_meta_ids as $mid ) { delete_metadata_by_mid( 'post', $mid ); } /** This action is documented in wp-includes/post.php */ do_action( 'delete_post', $post_id, $post ); $result = $wpdb->delete( $wpdb->posts, array( 'ID' => $post_id ) ); if ( ! $result ) { return false; } /** This action is documented in wp-includes/post.php */ do_action( 'deleted_post', $post_id, $post ); wp_delete_attachment_files( $post_id, $meta, $backup_sizes, $file ); clean_post_cache( $post ); return $post; } /** * Deletes all files that belong to the given attachment. * * @since 4.9.7 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $post_id Attachment ID. * @param array $meta The attachment's meta data. * @param array $backup_sizes The meta data for the attachment's backup images. * @param string $file Absolute path to the attachment's file. * @return bool True on success, false on failure. */ function wp_delete_attachment_files( $post_id, $meta, $backup_sizes, $file ) { global $wpdb; $uploadpath = wp_get_upload_dir(); $deleted = true; if ( ! empty( $meta['thumb'] ) ) { // Don't delete the thumb if another attachment uses it. if ( ! $wpdb->get_row( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attachment_metadata' AND meta_value LIKE %s AND post_id <> %d", '%' . $wpdb->esc_like( $meta['thumb'] ) . '%', $post_id ) ) ) { $thumbfile = str_replace( wp_basename( $file ), $meta['thumb'], $file ); if ( ! empty( $thumbfile ) ) { $thumbfile = path_join( $uploadpath['basedir'], $thumbfile ); $thumbdir = path_join( $uploadpath['basedir'], dirname( $file ) ); if ( ! wp_delete_file_from_directory( $thumbfile, $thumbdir ) ) { $deleted = false; } } } } // Remove intermediate and backup images if there are any. if ( isset( $meta['sizes'] ) && is_array( $meta['sizes'] ) ) { $intermediate_dir = path_join( $uploadpath['basedir'], dirname( $file ) ); foreach ( $meta['sizes'] as $size => $sizeinfo ) { $intermediate_file = str_replace( wp_basename( $file ), $sizeinfo['file'], $file ); if ( ! empty( $intermediate_file ) ) { $intermediate_file = path_join( $uploadpath['basedir'], $intermediate_file ); if ( ! wp_delete_file_from_directory( $intermediate_file, $intermediate_dir ) ) { $deleted = false; } } } } if ( ! empty( $meta['original_image'] ) ) { if ( empty( $intermediate_dir ) ) { $intermediate_dir = path_join( $uploadpath['basedir'], dirname( $file ) ); } $original_image = str_replace( wp_basename( $file ), $meta['original_image'], $file ); if ( ! empty( $original_image ) ) { $original_image = path_join( $uploadpath['basedir'], $original_image ); if ( ! wp_delete_file_from_directory( $original_image, $intermediate_dir ) ) { $deleted = false; } } } if ( is_array( $backup_sizes ) ) { $del_dir = path_join( $uploadpath['basedir'], dirname( $meta['file'] ) ); foreach ( $backup_sizes as $size ) { $del_file = path_join( dirname( $meta['file'] ), $size['file'] ); if ( ! empty( $del_file ) ) { $del_file = path_join( $uploadpath['basedir'], $del_file ); if ( ! wp_delete_file_from_directory( $del_file, $del_dir ) ) { $deleted = false; } } } } if ( ! wp_delete_file_from_directory( $file, $uploadpath['basedir'] ) ) { $deleted = false; } return $deleted; } /** * Retrieves attachment metadata for attachment ID. * * @since 2.1.0 * @since 6.0.0 The `$filesize` value was added to the returned array. * * @param int $attachment_id Attachment post ID. Defaults to global $post. * @param bool $unfiltered Optional. If true, filters are not run. Default false. * @return array|false { * Attachment metadata. False on failure. * * @type int $width The width of the attachment. * @type int $height The height of the attachment. * @type string $file The file path relative to `wp-content/uploads`. * @type array $sizes Keys are size slugs, each value is an array containing * 'file', 'width', 'height', and 'mime-type'. * @type array $image_meta Image metadata. * @type int $filesize File size of the attachment. * } */ function wp_get_attachment_metadata( $attachment_id = 0, $unfiltered = false ) { $attachment_id = (int) $attachment_id; if ( ! $attachment_id ) { $post = get_post(); if ( ! $post ) { return false; } $attachment_id = $post->ID; } $data = get_post_meta( $attachment_id, '_wp_attachment_metadata', true ); if ( ! $data ) { return false; } if ( $unfiltered ) { return $data; } /** * Filters the attachment meta data. * * @since 2.1.0 * * @param array $data Array of meta data for the given attachment. * @param int $attachment_id Attachment post ID. */ return apply_filters( 'wp_get_attachment_metadata', $data, $attachment_id ); } /** * Updates metadata for an attachment. * * @since 2.1.0 * * @param int $attachment_id Attachment post ID. * @param array $data Attachment meta data. * @return int|false False if $post is invalid. */ function wp_update_attachment_metadata( $attachment_id, $data ) { $attachment_id = (int) $attachment_id; $post = get_post( $attachment_id ); if ( ! $post ) { return false; } /** * Filters the updated attachment meta data. * * @since 2.1.0 * * @param array $data Array of updated attachment meta data. * @param int $attachment_id Attachment post ID. */ $data = apply_filters( 'wp_update_attachment_metadata', $data, $post->ID ); if ( $data ) { return update_post_meta( $post->ID, '_wp_attachment_metadata', $data ); } else { return delete_post_meta( $post->ID, '_wp_attachment_metadata' ); } } /** * Retrieves the URL for an attachment. * * @since 2.1.0 * * @global string $pagenow The filename of the current screen. * * @param int $attachment_id Optional. Attachment post ID. Defaults to global $post. * @return string|false Attachment URL, otherwise false. */ function wp_get_attachment_url( $attachment_id = 0 ) { global $pagenow; $attachment_id = (int) $attachment_id; $post = get_post( $attachment_id ); if ( ! $post ) { return false; } if ( 'attachment' !== $post->post_type ) { return false; } $url = ''; // Get attached file. $file = get_post_meta( $post->ID, '_wp_attached_file', true ); if ( $file ) { // Get upload directory. $uploads = wp_get_upload_dir(); if ( $uploads && false === $uploads['error'] ) { // Check that the upload base exists in the file location. if ( str_starts_with( $file, $uploads['basedir'] ) ) { // Replace file location with url location. $url = str_replace( $uploads['basedir'], $uploads['baseurl'], $file ); } elseif ( str_contains( $file, 'wp-content/uploads' ) ) { // Get the directory name relative to the basedir (back compat for pre-2.7 uploads). $url = trailingslashit( $uploads['baseurl'] . '/' . _wp_get_attachment_relative_path( $file ) ) . wp_basename( $file ); } else { // It's a newly-uploaded file, therefore $file is relative to the basedir. $url = $uploads['baseurl'] . "/$file"; } } } /* * If any of the above options failed, Fallback on the GUID as used pre-2.7, * not recommended to rely upon this. */ if ( ! $url ) { $url = get_the_guid( $post->ID ); } // On SSL front end, URLs should be HTTPS. if ( is_ssl() && ! is_admin() && 'wp-login.php' !== $pagenow ) { $url = set_url_scheme( $url ); } /** * Filters the attachment URL. * * @since 2.1.0 * * @param string $url URL for the given attachment. * @param int $attachment_id Attachment post ID. */ $url = apply_filters( 'wp_get_attachment_url', $url, $post->ID ); if ( ! $url ) { return false; } return $url; } /** * Retrieves the caption for an attachment. * * @since 4.6.0 * * @param int $post_id Optional. Attachment ID. Default is the ID of the global `$post`. * @return string|false Attachment caption on success, false on failure. */ function wp_get_attachment_caption( $post_id = 0 ) { $post_id = (int) $post_id; $post = get_post( $post_id ); if ( ! $post ) { return false; } if ( 'attachment' !== $post->post_type ) { return false; } $caption = $post->post_excerpt; /** * Filters the attachment caption. * * @since 4.6.0 * * @param string $caption Caption for the given attachment. * @param int $post_id Attachment ID. */ return apply_filters( 'wp_get_attachment_caption', $caption, $post->ID ); } /** * Retrieves URL for an attachment thumbnail. * * @since 2.1.0 * @since 6.1.0 Changed to use wp_get_attachment_image_url(). * * @param int $post_id Optional. Attachment ID. Default is the ID of the global `$post`. * @return string|false Thumbnail URL on success, false on failure. */ function wp_get_attachment_thumb_url( $post_id = 0 ) { $post_id = (int) $post_id; /* * This uses image_downsize() which also looks for the (very) old format $image_meta['thumb'] * when the newer format $image_meta['sizes']['thumbnail'] doesn't exist. */ $thumbnail_url = wp_get_attachment_image_url( $post_id, 'thumbnail' ); if ( empty( $thumbnail_url ) ) { return false; } /** * Filters the attachment thumbnail URL. * * @since 2.1.0 * * @param string $thumbnail_url URL for the attachment thumbnail. * @param int $post_id Attachment ID. */ return apply_filters( 'wp_get_attachment_thumb_url', $thumbnail_url, $post_id ); } /** * Verifies an attachment is of a given type. * * @since 4.2.0 * * @param string $type Attachment type. Accepts 'image', 'audio', or 'video'. * @param int|WP_Post $post Optional. Attachment ID or object. Default is global $post. * @return bool True if one of the accepted types, false otherwise. */ function wp_attachment_is( $type, $post = null ) { $post = get_post( $post ); if ( ! $post ) { return false; } $file = get_attached_file( $post->ID ); if ( ! $file ) { return false; } if ( str_starts_with( $post->post_mime_type, $type . '/' ) ) { return true; } $check = wp_check_filetype( $file ); if ( empty( $check['ext'] ) ) { return false; } $ext = $check['ext']; if ( 'import' !== $post->post_mime_type ) { return $type === $ext; } switch ( $type ) { case 'image': $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp' ); return in_array( $ext, $image_exts, true ); case 'audio': return in_array( $ext, wp_get_audio_extensions(), true ); case 'video': return in_array( $ext, wp_get_video_extensions(), true ); default: return $type === $ext; } } /** * Determines whether an attachment is an image. * * For more information on this and similar theme functions, check out * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ * Conditional Tags} article in the Theme Developer Handbook. * * @since 2.1.0 * @since 4.2.0 Modified into wrapper for wp_attachment_is() and * allowed WP_Post object to be passed. * * @param int|WP_Post $post Optional. Attachment ID or object. Default is global $post. * @return bool Whether the attachment is an image. */ function wp_attachment_is_image( $post = null ) { return wp_attachment_is( 'image', $post ); } /** * Retrieves the icon for a MIME type or attachment. * * @since 2.1.0 * * @param string|int $mime MIME type or attachment ID. * @return string|false Icon, false otherwise. */ function wp_mime_type_icon( $mime = 0 ) { if ( ! is_numeric( $mime ) ) { $icon = wp_cache_get( "mime_type_icon_$mime" ); } $post_id = 0; if ( empty( $icon ) ) { $post_mimes = array(); if ( is_numeric( $mime ) ) { $mime = (int) $mime; $post = get_post( $mime ); if ( $post ) { $post_id = (int) $post->ID; $file = get_attached_file( $post_id ); $ext = preg_replace( '/^.+?\.([^.]+)$/', '$1', $file ); if ( ! empty( $ext ) ) { $post_mimes[] = $ext; $ext_type = wp_ext2type( $ext ); if ( $ext_type ) { $post_mimes[] = $ext_type; } } $mime = $post->post_mime_type; } else { $mime = 0; } } else { $post_mimes[] = $mime; } $icon_files = wp_cache_get( 'icon_files' ); if ( ! is_array( $icon_files ) ) { /** * Filters the icon directory path. * * @since 2.0.0 * * @param string $path Icon directory absolute path. */ $icon_dir = apply_filters( 'icon_dir', ABSPATH . WPINC . '/images/media' ); /** * Filters the icon directory URI. * * @since 2.0.0 * * @param string $uri Icon directory URI. */ $icon_dir_uri = apply_filters( 'icon_dir_uri', includes_url( 'images/media' ) ); /** * Filters the array of icon directory URIs. * * @since 2.5.0 * * @param string[] $uris Array of icon directory URIs keyed by directory absolute path. */ $dirs = apply_filters( 'icon_dirs', array( $icon_dir => $icon_dir_uri ) ); $icon_files = array(); while ( $dirs ) { $keys = array_keys( $dirs ); $dir = array_shift( $keys ); $uri = array_shift( $dirs ); $dh = opendir( $dir ); if ( $dh ) { while ( false !== $file = readdir( $dh ) ) { $file = wp_basename( $file ); if ( str_starts_with( $file, '.' ) ) { continue; } $ext = strtolower( substr( $file, -4 ) ); if ( ! in_array( $ext, array( '.png', '.gif', '.jpg' ), true ) ) { if ( is_dir( "$dir/$file" ) ) { $dirs[ "$dir/$file" ] = "$uri/$file"; } continue; } $icon_files[ "$dir/$file" ] = "$uri/$file"; } closedir( $dh ); } } wp_cache_add( 'icon_files', $icon_files, 'default', 600 ); } $types = array(); // Icon wp_basename - extension = MIME wildcard. foreach ( $icon_files as $file => $uri ) { $types[ preg_replace( '/^([^.]*).*$/', '$1', wp_basename( $file ) ) ] =& $icon_files[ $file ]; } if ( ! empty( $mime ) ) { $post_mimes[] = substr( $mime, 0, strpos( $mime, '/' ) ); $post_mimes[] = substr( $mime, strpos( $mime, '/' ) + 1 ); $post_mimes[] = str_replace( '/', '_', $mime ); } $matches = wp_match_mime_types( array_keys( $types ), $post_mimes ); $matches['default'] = array( 'default' ); foreach ( $matches as $match => $wilds ) { foreach ( $wilds as $wild ) { if ( ! isset( $types[ $wild ] ) ) { continue; } $icon = $types[ $wild ]; if ( ! is_numeric( $mime ) ) { wp_cache_add( "mime_type_icon_$mime", $icon ); } break 2; } } } /** * Filters the mime type icon. * * @since 2.1.0 * * @param string $icon Path to the mime type icon. * @param string $mime Mime type. * @param int $post_id Attachment ID. Will equal 0 if the function passed * the mime type. */ return apply_filters( 'wp_mime_type_icon', $icon, $mime, $post_id ); } /** * Checks for changed slugs for published post objects and save the old slug. * * The function is used when a post object of any type is updated, * by comparing the current and previous post objects. * * If the slug was changed and not already part of the old slugs then it will be * added to the post meta field ('_wp_old_slug') for storing old slugs for that * post. * * The most logically usage of this function is redirecting changed post objects, so * that those that linked to an changed post will be redirected to the new post. * * @since 2.1.0 * * @param int $post_id Post ID. * @param WP_Post $post The post object. * @param WP_Post $post_before The previous post object. */ function wp_check_for_changed_slugs( $post_id, $post, $post_before ) { // Don't bother if it hasn't changed. if ( $post->post_name == $post_before->post_name ) { return; } // We're only concerned with published, non-hierarchical objects. if ( ! ( 'publish' === $post->post_status || ( 'attachment' === get_post_type( $post ) && 'inherit' === $post->post_status ) ) || is_post_type_hierarchical( $post->post_type ) ) { return; } $old_slugs = (array) get_post_meta( $post_id, '_wp_old_slug' ); // If we haven't added this old slug before, add it now. if ( ! empty( $post_before->post_name ) && ! in_array( $post_before->post_name, $old_slugs, true ) ) { add_post_meta( $post_id, '_wp_old_slug', $post_before->post_name ); } // If the new slug was used previously, delete it from the list. if ( in_array( $post->post_name, $old_slugs, true ) ) { delete_post_meta( $post_id, '_wp_old_slug', $post->post_name ); } } /** * Checks for changed dates for published post objects and save the old date. * * The function is used when a post object of any type is updated, * by comparing the current and previous post objects. * * If the date was changed and not already part of the old dates then it will be * added to the post meta field ('_wp_old_date') for storing old dates for that * post. * * The most logically usage of this function is redirecting changed post objects, so * that those that linked to an changed post will be redirected to the new post. * * @since 4.9.3 * * @param int $post_id Post ID. * @param WP_Post $post The post object. * @param WP_Post $post_before The previous post object. */ function wp_check_for_changed_dates( $post_id, $post, $post_before ) { $previous_date = gmdate( 'Y-m-d', strtotime( $post_before->post_date ) ); $new_date = gmdate( 'Y-m-d', strtotime( $post->post_date ) ); // Don't bother if it hasn't changed. if ( $new_date == $previous_date ) { return; } // We're only concerned with published, non-hierarchical objects. if ( ! ( 'publish' === $post->post_status || ( 'attachment' === get_post_type( $post ) && 'inherit' === $post->post_status ) ) || is_post_type_hierarchical( $post->post_type ) ) { return; } $old_dates = (array) get_post_meta( $post_id, '_wp_old_date' ); // If we haven't added this old date before, add it now. if ( ! empty( $previous_date ) && ! in_array( $previous_date, $old_dates, true ) ) { add_post_meta( $post_id, '_wp_old_date', $previous_date ); } // If the new slug was used previously, delete it from the list. if ( in_array( $new_date, $old_dates, true ) ) { delete_post_meta( $post_id, '_wp_old_date', $new_date ); } } /** * Retrieves the private post SQL based on capability. * * This function provides a standardized way to appropriately select on the * post_status of a post type. The function will return a piece of SQL code * that can be added to a WHERE clause; this SQL is constructed to allow all * published posts, and all private posts to which the user has access. * * @since 2.2.0 * @since 4.3.0 Added the ability to pass an array to `$post_type`. * * @param string|array $post_type Single post type or an array of post types. Currently only supports 'post' or 'page'. * @return string SQL code that can be added to a where clause. */ function get_private_posts_cap_sql( $post_type ) { return get_posts_by_author_sql( $post_type, false ); } /** * Retrieves the post SQL based on capability, author, and type. * * @since 3.0.0 * @since 4.3.0 Introduced the ability to pass an array of post types to `$post_type`. * * @see get_private_posts_cap_sql() * @global wpdb $wpdb WordPress database abstraction object. * * @param string|string[] $post_type Single post type or an array of post types. * @param bool $full Optional. Returns a full WHERE statement instead of just * an 'andalso' term. Default true. * @param int $post_author Optional. Query posts having a single author ID. Default null. * @param bool $public_only Optional. Only return public posts. Skips cap checks for * $current_user. Default false. * @return string SQL WHERE code that can be added to a query. */ function get_posts_by_author_sql( $post_type, $full = true, $post_author = null, $public_only = false ) { global $wpdb; if ( is_array( $post_type ) ) { $post_types = $post_type; } else { $post_types = array( $post_type ); } $post_type_clauses = array(); foreach ( $post_types as $post_type ) { $post_type_obj = get_post_type_object( $post_type ); if ( ! $post_type_obj ) { continue; } /** * Filters the capability to read private posts for a custom post type * when generating SQL for getting posts by author. * * @since 2.2.0 * @deprecated 3.2.0 The hook transitioned from "somewhat useless" to "totally useless". * * @param string $cap Capability. */ $cap = apply_filters_deprecated( 'pub_priv_sql_capability', array( '' ), '3.2.0' ); if ( ! $cap ) { $cap = current_user_can( $post_type_obj->cap->read_private_posts ); } // Only need to check the cap if $public_only is false. $post_status_sql = "post_status = 'publish'"; if ( false === $public_only ) { if ( $cap ) { // Does the user have the capability to view private posts? Guess so. $post_status_sql .= " OR post_status = 'private'"; } elseif ( is_user_logged_in() ) { // Users can view their own private posts. $id = get_current_user_id(); if ( null === $post_author || ! $full ) { $post_status_sql .= " OR post_status = 'private' AND post_author = $id"; } elseif ( $id == (int) $post_author ) { $post_status_sql .= " OR post_status = 'private'"; } // Else none. } // Else none. } $post_type_clauses[] = "( post_type = '" . $post_type . "' AND ( $post_status_sql ) )"; } if ( empty( $post_type_clauses ) ) { return $full ? 'WHERE 1 = 0' : '1 = 0'; } $sql = '( ' . implode( ' OR ', $post_type_clauses ) . ' )'; if ( null !== $post_author ) { $sql .= $wpdb->prepare( ' AND post_author = %d', $post_author ); } if ( $full ) { $sql = 'WHERE ' . $sql; } return $sql; } /** * Retrieves the most recent time that a post on the site was published. * * The server timezone is the default and is the difference between GMT and * server time. The 'blog' value is the date when the last post was posted. * The 'gmt' is when the last post was posted in GMT formatted date. * * @since 0.71 * @since 4.4.0 The `$post_type` argument was added. * * @param string $timezone Optional. The timezone for the timestamp. Accepts 'server', 'blog', or 'gmt'. * 'server' uses the server's internal timezone. * 'blog' uses the `post_date` field, which proxies to the timezone set for the site. * 'gmt' uses the `post_date_gmt` field. * Default 'server'. * @param string $post_type Optional. The post type to check. Default 'any'. * @return string The date of the last post, or false on failure. */ function get_lastpostdate( $timezone = 'server', $post_type = 'any' ) { $lastpostdate = _get_last_post_time( $timezone, 'date', $post_type ); /** * Filters the most recent time that a post on the site was published. * * @since 2.3.0 * @since 5.5.0 Added the `$post_type` parameter. * * @param string|false $lastpostdate The most recent time that a post was published, * in 'Y-m-d H:i:s' format. False on failure. * @param string $timezone Location to use for getting the post published date. * See get_lastpostdate() for accepted `$timezone` values. * @param string $post_type The post type to check. */ return apply_filters( 'get_lastpostdate', $lastpostdate, $timezone, $post_type ); } /** * Gets the most recent time that a post on the site was modified. * * The server timezone is the default and is the difference between GMT and * server time. The 'blog' value is just when the last post was modified. * The 'gmt' is when the last post was modified in GMT time. * * @since 1.2.0 * @since 4.4.0 The `$post_type` argument was added. * * @param string $timezone Optional. The timezone for the timestamp. See get_lastpostdate() * for information on accepted values. * Default 'server'. * @param string $post_type Optional. The post type to check. Default 'any'. * @return string The timestamp in 'Y-m-d H:i:s' format, or false on failure. */ function get_lastpostmodified( $timezone = 'server', $post_type = 'any' ) { /** * Pre-filter the return value of get_lastpostmodified() before the query is run. * * @since 4.4.0 * * @param string|false $lastpostmodified The most recent time that a post was modified, * in 'Y-m-d H:i:s' format, or false. Returning anything * other than false will short-circuit the function. * @param string $timezone Location to use for getting the post modified date. * See get_lastpostdate() for accepted `$timezone` values. * @param string $post_type The post type to check. */ $lastpostmodified = apply_filters( 'pre_get_lastpostmodified', false, $timezone, $post_type ); if ( false !== $lastpostmodified ) { return $lastpostmodified; } $lastpostmodified = _get_last_post_time( $timezone, 'modified', $post_type ); $lastpostdate = get_lastpostdate( $timezone, $post_type ); if ( $lastpostdate > $lastpostmodified ) { $lastpostmodified = $lastpostdate; } /** * Filters the most recent time that a post on the site was modified. * * @since 2.3.0 * @since 5.5.0 Added the `$post_type` parameter. * * @param string|false $lastpostmodified The most recent time that a post was modified, * in 'Y-m-d H:i:s' format. False on failure. * @param string $timezone Location to use for getting the post modified date. * See get_lastpostdate() for accepted `$timezone` values. * @param string $post_type The post type to check. */ return apply_filters( 'get_lastpostmodified', $lastpostmodified, $timezone, $post_type ); } /** * Gets the timestamp of the last time any post was modified or published. * * @since 3.1.0 * @since 4.4.0 The `$post_type` argument was added. * @access private * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $timezone The timezone for the timestamp. See get_lastpostdate(). * for information on accepted values. * @param string $field Post field to check. Accepts 'date' or 'modified'. * @param string $post_type Optional. The post type to check. Default 'any'. * @return string|false The timestamp in 'Y-m-d H:i:s' format, or false on failure. */ function _get_last_post_time( $timezone, $field, $post_type = 'any' ) { global $wpdb; if ( ! in_array( $field, array( 'date', 'modified' ), true ) ) { return false; } $timezone = strtolower( $timezone ); $key = "lastpost{$field}:$timezone"; if ( 'any' !== $post_type ) { $key .= ':' . sanitize_key( $post_type ); } $date = wp_cache_get( $key, 'timeinfo' ); if ( false !== $date ) { return $date; } if ( 'any' === $post_type ) { $post_types = get_post_types( array( 'public' => true ) ); array_walk( $post_types, array( $wpdb, 'escape_by_ref' ) ); $post_types = "'" . implode( "', '", $post_types ) . "'"; } else { $post_types = "'" . sanitize_key( $post_type ) . "'"; } switch ( $timezone ) { case 'gmt': $date = $wpdb->get_var( "SELECT post_{$field}_gmt FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ({$post_types}) ORDER BY post_{$field}_gmt DESC LIMIT 1" ); break; case 'blog': $date = $wpdb->get_var( "SELECT post_{$field} FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ({$post_types}) ORDER BY post_{$field}_gmt DESC LIMIT 1" ); break; case 'server': $add_seconds_server = gmdate( 'Z' ); $date = $wpdb->get_var( "SELECT DATE_ADD(post_{$field}_gmt, INTERVAL '$add_seconds_server' SECOND) FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ({$post_types}) ORDER BY post_{$field}_gmt DESC LIMIT 1" ); break; } if ( $date ) { wp_cache_set( $key, $date, 'timeinfo' ); return $date; } return false; } /** * Updates posts in cache. * * @since 1.5.1 * * @param WP_Post[] $posts Array of post objects (passed by reference). */ function update_post_cache( &$posts ) { if ( ! $posts ) { return; } $data = array(); foreach ( $posts as $post ) { if ( empty( $post->filter ) || 'raw' !== $post->filter ) { $post = sanitize_post( $post, 'raw' ); } $data[ $post->ID ] = $post; } wp_cache_add_multiple( $data, 'posts' ); } /** * Will clean the post in the cache. * * Cleaning means delete from the cache of the post. Will call to clean the term * object cache associated with the post ID. * * This function not run if $_wp_suspend_cache_invalidation is not empty. See * wp_suspend_cache_invalidation(). * * @since 2.0.0 * * @global bool $_wp_suspend_cache_invalidation * * @param int|WP_Post $post Post ID or post object to remove from the cache. */ function clean_post_cache( $post ) { global $_wp_suspend_cache_invalidation; if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } $post = get_post( $post ); if ( ! $post ) { return; } wp_cache_delete( $post->ID, 'posts' ); wp_cache_delete( 'post_parent:' . (string) $post->ID, 'posts' ); wp_cache_delete( $post->ID, 'post_meta' ); clean_object_term_cache( $post->ID, $post->post_type ); wp_cache_delete( 'wp_get_archives', 'general' ); /** * Fires immediately after the given post's cache is cleaned. * * @since 2.5.0 * * @param int $post_id Post ID. * @param WP_Post $post Post object. */ do_action( 'clean_post_cache', $post->ID, $post ); if ( 'page' === $post->post_type ) { wp_cache_delete( 'all_page_ids', 'posts' ); /** * Fires immediately after the given page's cache is cleaned. * * @since 2.5.0 * * @param int $post_id Post ID. */ do_action( 'clean_page_cache', $post->ID ); } wp_cache_set_posts_last_changed(); } /** * Updates post, term, and metadata caches for a list of post objects. * * @since 1.5.0 * * @param WP_Post[] $posts Array of post objects (passed by reference). * @param string $post_type Optional. Post type. Default 'post'. * @param bool $update_term_cache Optional. Whether to update the term cache. Default true. * @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true. */ function update_post_caches( &$posts, $post_type = 'post', $update_term_cache = true, $update_meta_cache = true ) { // No point in doing all this work if we didn't match any posts. if ( ! $posts ) { return; } update_post_cache( $posts ); $post_ids = array(); foreach ( $posts as $post ) { $post_ids[] = $post->ID; } if ( ! $post_type ) { $post_type = 'any'; } if ( $update_term_cache ) { if ( is_array( $post_type ) ) { $ptypes = $post_type; } elseif ( 'any' === $post_type ) { $ptypes = array(); // Just use the post_types in the supplied posts. foreach ( $posts as $post ) { $ptypes[] = $post->post_type; } $ptypes = array_unique( $ptypes ); } else { $ptypes = array( $post_type ); } if ( ! empty( $ptypes ) ) { update_object_term_cache( $post_ids, $ptypes ); } } if ( $update_meta_cache ) { update_postmeta_cache( $post_ids ); } } /** * Updates post author user caches for a list of post objects. * * @since 6.1.0 * * @param WP_Post[] $posts Array of post objects. */ function update_post_author_caches( $posts ) { /* * cache_users() is a pluggable function so is not available prior * to the `plugins_loaded` hook firing. This is to ensure against * fatal errors when the function is not available. */ if ( ! function_exists( 'cache_users' ) ) { return; } $author_ids = wp_list_pluck( $posts, 'post_author' ); $author_ids = array_map( 'absint', $author_ids ); $author_ids = array_unique( array_filter( $author_ids ) ); cache_users( $author_ids ); } /** * Updates parent post caches for a list of post objects. * * @since 6.1.0 * * @param WP_Post[] $posts Array of post objects. */ function update_post_parent_caches( $posts ) { $parent_ids = wp_list_pluck( $posts, 'post_parent' ); $parent_ids = array_map( 'absint', $parent_ids ); $parent_ids = array_unique( array_filter( $parent_ids ) ); if ( ! empty( $parent_ids ) ) { _prime_post_caches( $parent_ids, false ); } } /** * Updates metadata cache for a list of post IDs. * * Performs SQL query to retrieve the metadata for the post IDs and updates the * metadata cache for the posts. Therefore, the functions, which call this * function, do not need to perform SQL queries on their own. * * @since 2.1.0 * * @param int[] $post_ids Array of post IDs. * @return array|false An array of metadata on success, false if there is nothing to update. */ function update_postmeta_cache( $post_ids ) { return update_meta_cache( 'post', $post_ids ); } /** * Will clean the attachment in the cache. * * Cleaning means delete from the cache. Optionally will clean the term * object cache associated with the attachment ID. * * This function will not run if $_wp_suspend_cache_invalidation is not empty. * * @since 3.0.0 * * @global bool $_wp_suspend_cache_invalidation * * @param int $id The attachment ID in the cache to clean. * @param bool $clean_terms Optional. Whether to clean terms cache. Default false. */ function clean_attachment_cache( $id, $clean_terms = false ) { global $_wp_suspend_cache_invalidation; if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } $id = (int) $id; wp_cache_delete( $id, 'posts' ); wp_cache_delete( $id, 'post_meta' ); if ( $clean_terms ) { clean_object_term_cache( $id, 'attachment' ); } /** * Fires after the given attachment's cache is cleaned. * * @since 3.0.0 * * @param int $id Attachment ID. */ do_action( 'clean_attachment_cache', $id ); } // // Hooks. // /** * Hook for managing future post transitions to published. * * @since 2.3.0 * @access private * * @see wp_clear_scheduled_hook() * @global wpdb $wpdb WordPress database abstraction object. * * @param string $new_status New post status. * @param string $old_status Previous post status. * @param WP_Post $post Post object. */ function _transition_post_status( $new_status, $old_status, $post ) { global $wpdb; if ( 'publish' !== $old_status && 'publish' === $new_status ) { // Reset GUID if transitioning to publish and it is empty. if ( '' === get_the_guid( $post->ID ) ) { $wpdb->update( $wpdb->posts, array( 'guid' => get_permalink( $post->ID ) ), array( 'ID' => $post->ID ) ); } /** * Fires when a post's status is transitioned from private to published. * * @since 1.5.0 * @deprecated 2.3.0 Use {@see 'private_to_publish'} instead. * * @param int $post_id Post ID. */ do_action_deprecated( 'private_to_published', array( $post->ID ), '2.3.0', 'private_to_publish' ); } // If published posts changed clear the lastpostmodified cache. if ( 'publish' === $new_status || 'publish' === $old_status ) { foreach ( array( 'server', 'gmt', 'blog' ) as $timezone ) { wp_cache_delete( "lastpostmodified:$timezone", 'timeinfo' ); wp_cache_delete( "lastpostdate:$timezone", 'timeinfo' ); wp_cache_delete( "lastpostdate:$timezone:{$post->post_type}", 'timeinfo' ); } } if ( $new_status !== $old_status ) { wp_cache_delete( _count_posts_cache_key( $post->post_type ), 'counts' ); wp_cache_delete( _count_posts_cache_key( $post->post_type, 'readable' ), 'counts' ); } // Always clears the hook in case the post status bounced from future to draft. wp_clear_scheduled_hook( 'publish_future_post', array( $post->ID ) ); } /** * Hook used to schedule publication for a post marked for the future. * * The $post properties used and must exist are 'ID' and 'post_date_gmt'. * * @since 2.3.0 * @access private * * @param int $deprecated Not used. Can be set to null. Never implemented. Not marked * as deprecated with _deprecated_argument() as it conflicts with * wp_transition_post_status() and the default filter for _future_post_hook(). * @param WP_Post $post Post object. */ function _future_post_hook( $deprecated, $post ) { wp_clear_scheduled_hook( 'publish_future_post', array( $post->ID ) ); wp_schedule_single_event( strtotime( get_gmt_from_date( $post->post_date ) . ' GMT' ), 'publish_future_post', array( $post->ID ) ); } /** * Hook to schedule pings and enclosures when a post is published. * * Uses XMLRPC_REQUEST and WP_IMPORTING constants. * * @since 2.3.0 * @access private * * @param int $post_id The ID of the post being published. */ function _publish_post_hook( $post_id ) { if ( defined( 'XMLRPC_REQUEST' ) ) { /** * Fires when _publish_post_hook() is called during an XML-RPC request. * * @since 2.1.0 * * @param int $post_id Post ID. */ do_action( 'xmlrpc_publish_post', $post_id ); } if ( defined( 'WP_IMPORTING' ) ) { return; } if ( get_option( 'default_pingback_flag' ) ) { add_post_meta( $post_id, '_pingme', '1', true ); } add_post_meta( $post_id, '_encloseme', '1', true ); $to_ping = get_to_ping( $post_id ); if ( ! empty( $to_ping ) ) { add_post_meta( $post_id, '_trackbackme', '1' ); } if ( ! wp_next_scheduled( 'do_pings' ) ) { wp_schedule_single_event( time(), 'do_pings' ); } } /** * Returns the ID of the post's parent. * * @since 3.1.0 * @since 5.9.0 The `$post` parameter was made optional. * * @param int|WP_Post|null $post Optional. Post ID or post object. Defaults to global $post. * @return int|false Post parent ID (which can be 0 if there is no parent), * or false if the post does not exist. */ function wp_get_post_parent_id( $post = null ) { $post = get_post( $post ); if ( ! $post || is_wp_error( $post ) ) { return false; } return (int) $post->post_parent; } /** * Checks the given subset of the post hierarchy for hierarchy loops. * * Prevents loops from forming and breaks those that it finds. Attached * to the {@see 'wp_insert_post_parent'} filter. * * @since 3.1.0 * * @see wp_find_hierarchy_loop() * * @param int $post_parent ID of the parent for the post we're checking. * @param int $post_id ID of the post we're checking. * @return int The new post_parent for the post, 0 otherwise. */ function wp_check_post_hierarchy_for_loops( $post_parent, $post_id ) { // Nothing fancy here - bail. if ( ! $post_parent ) { return 0; } // New post can't cause a loop. if ( ! $post_id ) { return $post_parent; } // Can't be its own parent. if ( $post_parent == $post_id ) { return 0; } // Now look for larger loops. $loop = wp_find_hierarchy_loop( 'wp_get_post_parent_id', $post_id, $post_parent ); if ( ! $loop ) { return $post_parent; // No loop. } // Setting $post_parent to the given value causes a loop. if ( isset( $loop[ $post_id ] ) ) { return 0; } // There's a loop, but it doesn't contain $post_id. Break the loop. foreach ( array_keys( $loop ) as $loop_member ) { wp_update_post( array( 'ID' => $loop_member, 'post_parent' => 0, ) ); } return $post_parent; } /** * Sets the post thumbnail (featured image) for the given post. * * @since 3.1.0 * * @param int|WP_Post $post Post ID or post object where thumbnail should be attached. * @param int $thumbnail_id Thumbnail to attach. * @return int|bool True on success, false on failure. */ function set_post_thumbnail( $post, $thumbnail_id ) { $post = get_post( $post ); $thumbnail_id = absint( $thumbnail_id ); if ( $post && $thumbnail_id && get_post( $thumbnail_id ) ) { if ( wp_get_attachment_image( $thumbnail_id, 'thumbnail' ) ) { return update_post_meta( $post->ID, '_thumbnail_id', $thumbnail_id ); } else { return delete_post_meta( $post->ID, '_thumbnail_id' ); } } return false; } /** * Removes the thumbnail (featured image) from the given post. * * @since 3.3.0 * * @param int|WP_Post $post Post ID or post object from which the thumbnail should be removed. * @return bool True on success, false on failure. */ function delete_post_thumbnail( $post ) { $post = get_post( $post ); if ( $post ) { return delete_post_meta( $post->ID, '_thumbnail_id' ); } return false; } /** * Deletes auto-drafts for new posts that are > 7 days old. * * @since 3.4.0 * * @global wpdb $wpdb WordPress database abstraction object. */ function wp_delete_auto_drafts() { global $wpdb; // Cleanup old auto-drafts more than 7 days old. $old_posts = $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_status = 'auto-draft' AND DATE_SUB( NOW(), INTERVAL 7 DAY ) > post_date" ); foreach ( (array) $old_posts as $delete ) { // Force delete. wp_delete_post( $delete, true ); } } /** * Queues posts for lazy-loading of term meta. * * @since 4.5.0 * * @param WP_Post[] $posts Array of WP_Post objects. */ function wp_queue_posts_for_term_meta_lazyload( $posts ) { $post_type_taxonomies = array(); $prime_post_terms = array(); foreach ( $posts as $post ) { if ( ! ( $post instanceof WP_Post ) ) { continue; } if ( ! isset( $post_type_taxonomies[ $post->post_type ] ) ) { $post_type_taxonomies[ $post->post_type ] = get_object_taxonomies( $post->post_type ); } foreach ( $post_type_taxonomies[ $post->post_type ] as $taxonomy ) { $prime_post_terms[ $taxonomy ][] = $post->ID; } } $term_ids = array(); if ( $prime_post_terms ) { foreach ( $prime_post_terms as $taxonomy => $post_ids ) { $cached_term_ids = wp_cache_get_multiple( $post_ids, "{$taxonomy}_relationships" ); if ( is_array( $cached_term_ids ) ) { $cached_term_ids = array_filter( $cached_term_ids ); foreach ( $cached_term_ids as $_term_ids ) { // Backward compatibility for if a plugin is putting objects into the cache, rather than IDs. foreach ( $_term_ids as $term_id ) { if ( is_numeric( $term_id ) ) { $term_ids[] = (int) $term_id; } elseif ( isset( $term_id->term_id ) ) { $term_ids[] = (int) $term_id->term_id; } } } } } $term_ids = array_unique( $term_ids ); } wp_lazyload_term_meta( $term_ids ); } /** * Updates the custom taxonomies' term counts when a post's status is changed. * * For example, default posts term counts (for custom taxonomies) don't include * private / draft posts. * * @since 3.3.0 * @access private * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. */ function _update_term_count_on_transition_post_status( $new_status, $old_status, $post ) { // Update counts for the post's terms. foreach ( (array) get_object_taxonomies( $post->post_type ) as $taxonomy ) { $tt_ids = wp_get_object_terms( $post->ID, $taxonomy, array( 'fields' => 'tt_ids' ) ); wp_update_term_count( $tt_ids, $taxonomy ); } } /** * Adds any posts from the given IDs to the cache that do not already exist in cache. * * @since 3.4.0 * @since 6.1.0 This function is no longer marked as "private". * * @see update_post_cache() * @see update_postmeta_cache() * @see update_object_term_cache() * * @global wpdb $wpdb WordPress database abstraction object. * * @param int[] $ids ID list. * @param bool $update_term_cache Optional. Whether to update the term cache. Default true. * @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true. */ function _prime_post_caches( $ids, $update_term_cache = true, $update_meta_cache = true ) { global $wpdb; $non_cached_ids = _get_non_cached_ids( $ids, 'posts' ); if ( ! empty( $non_cached_ids ) ) { $fresh_posts = $wpdb->get_results( sprintf( "SELECT $wpdb->posts.* FROM $wpdb->posts WHERE ID IN (%s)", implode( ',', $non_cached_ids ) ) ); if ( $fresh_posts ) { // Despite the name, update_post_cache() expects an array rather than a single post. update_post_cache( $fresh_posts ); } } if ( $update_meta_cache ) { update_postmeta_cache( $ids ); } if ( $update_term_cache ) { $post_types = array_map( 'get_post_type', $ids ); $post_types = array_unique( $post_types ); update_object_term_cache( $ids, $post_types ); } } /** * Prime the cache containing the parent ID of various post objects. * * @global wpdb $wpdb WordPress database abstraction object. * * @since 6.4.0 * * @param int[] $ids ID list. */ function _prime_post_parent_id_caches( array $ids ) { global $wpdb; $ids = array_filter( $ids, '_validate_cache_id' ); $ids = array_unique( array_map( 'intval', $ids ), SORT_NUMERIC ); if ( empty( $ids ) ) { return; } $cache_keys = array(); foreach ( $ids as $id ) { $cache_keys[ $id ] = 'post_parent:' . (string) $id; } $cached_data = wp_cache_get_multiple( array_values( $cache_keys ), 'posts' ); $non_cached_ids = array(); foreach ( $cache_keys as $id => $cache_key ) { if ( false === $cached_data[ $cache_key ] ) { $non_cached_ids[] = $id; } } if ( ! empty( $non_cached_ids ) ) { $fresh_posts = $wpdb->get_results( sprintf( "SELECT $wpdb->posts.ID, $wpdb->posts.post_parent FROM $wpdb->posts WHERE ID IN (%s)", implode( ',', $non_cached_ids ) ) ); if ( $fresh_posts ) { $post_parent_data = array(); foreach ( $fresh_posts as $fresh_post ) { $post_parent_data[ 'post_parent:' . (string) $fresh_post->ID ] = (int) $fresh_post->post_parent; } wp_cache_add_multiple( $post_parent_data, 'posts' ); } } } /** * Adds a suffix if any trashed posts have a given slug. * * Store its desired (i.e. current) slug so it can try to reclaim it * if the post is untrashed. * * For internal use. * * @since 4.5.0 * @access private * * @param string $post_name Post slug. * @param int $post_id Optional. Post ID that should be ignored. Default 0. */ function wp_add_trashed_suffix_to_post_name_for_trashed_posts( $post_name, $post_id = 0 ) { $trashed_posts_with_desired_slug = get_posts( array( 'name' => $post_name, 'post_status' => 'trash', 'post_type' => 'any', 'nopaging' => true, 'post__not_in' => array( $post_id ), ) ); if ( ! empty( $trashed_posts_with_desired_slug ) ) { foreach ( $trashed_posts_with_desired_slug as $_post ) { wp_add_trashed_suffix_to_post_name_for_post( $_post ); } } } /** * Adds a trashed suffix for a given post. * * Store its desired (i.e. current) slug so it can try to reclaim it * if the post is untrashed. * * For internal use. * * @since 4.5.0 * @access private * * @global wpdb $wpdb WordPress database abstraction object. * * @param WP_Post $post The post. * @return string New slug for the post. */ function wp_add_trashed_suffix_to_post_name_for_post( $post ) { global $wpdb; $post = get_post( $post ); if ( str_ends_with( $post->post_name, '__trashed' ) ) { return $post->post_name; } add_post_meta( $post->ID, '_wp_desired_post_slug', $post->post_name ); $post_name = _truncate_post_slug( $post->post_name, 191 ) . '__trashed'; $wpdb->update( $wpdb->posts, array( 'post_name' => $post_name ), array( 'ID' => $post->ID ) ); clean_post_cache( $post->ID ); return $post_name; } /** * Sets the last changed time for the 'posts' cache group. * * @since 5.0.0 */ function wp_cache_set_posts_last_changed() { wp_cache_set_last_changed( 'posts' ); } /** * Gets all available post MIME types for a given post type. * * @since 2.5.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $type * @return string[] An array of MIME types. */ function get_available_post_mime_types( $type = 'attachment' ) { global $wpdb; /** * Filters the list of available post MIME types for the given post type. * * @since 6.4.0 * * @param string[]|null $mime_types An array of MIME types. Default null. * @param string $type The post type name. Usually 'attachment' but can be any post type. */ $mime_types = apply_filters( 'pre_get_available_post_mime_types', null, $type ); if ( ! is_array( $mime_types ) ) { $mime_types = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT post_mime_type FROM $wpdb->posts WHERE post_type = %s", $type ) ); } return $mime_types; } /** * Retrieves the path to an uploaded image file. * * Similar to `get_attached_file()` however some images may have been processed after uploading * to make them suitable for web use. In this case the attached "full" size file is usually replaced * with a scaled down version of the original image. This function always returns the path * to the originally uploaded image file. * * @since 5.3.0 * @since 5.4.0 Added the `$unfiltered` parameter. * * @param int $attachment_id Attachment ID. * @param bool $unfiltered Optional. Passed through to `get_attached_file()`. Default false. * @return string|false Path to the original image file or false if the attachment is not an image. */ function wp_get_original_image_path( $attachment_id, $unfiltered = false ) { if ( ! wp_attachment_is_image( $attachment_id ) ) { return false; } $image_meta = wp_get_attachment_metadata( $attachment_id ); $image_file = get_attached_file( $attachment_id, $unfiltered ); if ( empty( $image_meta['original_image'] ) ) { $original_image = $image_file; } else { $original_image = path_join( dirname( $image_file ), $image_meta['original_image'] ); } /** * Filters the path to the original image. * * @since 5.3.0 * * @param string $original_image Path to original image file. * @param int $attachment_id Attachment ID. */ return apply_filters( 'wp_get_original_image_path', $original_image, $attachment_id ); } /** * Retrieves the URL to an original attachment image. * * Similar to `wp_get_attachment_url()` however some images may have been * processed after uploading. In this case this function returns the URL * to the originally uploaded image file. * * @since 5.3.0 * * @param int $attachment_id Attachment post ID. * @return string|false Attachment image URL, false on error or if the attachment is not an image. */ function wp_get_original_image_url( $attachment_id ) { if ( ! wp_attachment_is_image( $attachment_id ) ) { return false; } $image_url = wp_get_attachment_url( $attachment_id ); if ( ! $image_url ) { return false; } $image_meta = wp_get_attachment_metadata( $attachment_id ); if ( empty( $image_meta['original_image'] ) ) { $original_image_url = $image_url; } else { $original_image_url = path_join( dirname( $image_url ), $image_meta['original_image'] ); } /** * Filters the URL to the original attachment image. * * @since 5.3.0 * * @param string $original_image_url URL to original image. * @param int $attachment_id Attachment ID. */ return apply_filters( 'wp_get_original_image_url', $original_image_url, $attachment_id ); } /** * Filters callback which sets the status of an untrashed post to its previous status. * * This can be used as a callback on the `wp_untrash_post_status` filter. * * @since 5.6.0 * * @param string $new_status The new status of the post being restored. * @param int $post_id The ID of the post being restored. * @param string $previous_status The status of the post at the point where it was trashed. * @return string The new status of the post. */ function wp_untrash_post_set_previous_status( $new_status, $post_id, $previous_status ) { return $previous_status; } /** * Returns whether the post can be edited in the block editor. * * @since 5.0.0 * @since 6.1.0 Moved to wp-includes from wp-admin. * * @param int|WP_Post $post Post ID or WP_Post object. * @return bool Whether the post can be edited in the block editor. */ function use_block_editor_for_post( $post ) { $post = get_post( $post ); if ( ! $post ) { return false; } // We're in the meta box loader, so don't use the block editor. if ( is_admin() && isset( $_GET['meta-box-loader'] ) ) { check_admin_referer( 'meta-box-loader', 'meta-box-loader-nonce' ); return false; } $use_block_editor = use_block_editor_for_post_type( $post->post_type ); /** * Filters whether a post is able to be edited in the block editor. * * @since 5.0.0 * * @param bool $use_block_editor Whether the post can be edited or not. * @param WP_Post $post The post being checked. */ return apply_filters( 'use_block_editor_for_post', $use_block_editor, $post ); } /** * Returns whether a post type is compatible with the block editor. * * The block editor depends on the REST API, and if the post type is not shown in the * REST API, then it won't work with the block editor. * * @since 5.0.0 * @since 6.1.0 Moved to wp-includes from wp-admin. * * @param string $post_type The post type. * @return bool Whether the post type can be edited with the block editor. */ function use_block_editor_for_post_type( $post_type ) { if ( ! post_type_exists( $post_type ) ) { return false; } if ( ! post_type_supports( $post_type, 'editor' ) ) { return false; } $post_type_object = get_post_type_object( $post_type ); if ( $post_type_object && ! $post_type_object->show_in_rest ) { return false; } /** * Filters whether a post is able to be edited in the block editor. * * @since 5.0.0 * * @param bool $use_block_editor Whether the post type can be edited or not. Default true. * @param string $post_type The post type being checked. */ return apply_filters( 'use_block_editor_for_post_type', true, $post_type ); } /** * Registers any additional post meta fields. * * @since 6.3.0 Adds `wp_pattern_sync_status` meta field to the wp_block post type so an unsynced option can be added. * * @link https://github.com/WordPress/gutenberg/pull/51144 */ function wp_create_initial_post_meta() { register_post_meta( 'wp_block', 'wp_pattern_sync_status', array( 'sanitize_callback' => 'sanitize_text_field', 'single' => true, 'type' => 'string', 'show_in_rest' => array( 'schema' => array( 'type' => 'string', 'enum' => array( 'partial', 'unsynced' ), ), ), ) ); } /** * Core Taxonomy API * * @package WordPress * @subpackage Taxonomy */ // // Taxonomy registration. // /** * Creates the initial taxonomies. * * This function fires twice: in wp-settings.php before plugins are loaded (for * backward compatibility reasons), and again on the {@see 'init'} action. We must * avoid registering rewrite rules before the {@see 'init'} action. * * @since 2.8.0 * @since 5.9.0 Added `'wp_template_part_area'` taxonomy. * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. */ function create_initial_taxonomies() { global $wp_rewrite; WP_Taxonomy::reset_default_labels(); if ( ! did_action( 'init' ) ) { $rewrite = array( 'category' => false, 'post_tag' => false, 'post_format' => false, ); } else { /** * Filters the post formats rewrite base. * * @since 3.1.0 * * @param string $context Context of the rewrite base. Default 'type'. */ $post_format_base = apply_filters( 'post_format_rewrite_base', 'type' ); $rewrite = array( 'category' => array( 'hierarchical' => true, 'slug' => get_option( 'category_base' ) ? get_option( 'category_base' ) : 'category', 'with_front' => ! get_option( 'category_base' ) || $wp_rewrite->using_index_permalinks(), 'ep_mask' => EP_CATEGORIES, ), 'post_tag' => array( 'hierarchical' => false, 'slug' => get_option( 'tag_base' ) ? get_option( 'tag_base' ) : 'tag', 'with_front' => ! get_option( 'tag_base' ) || $wp_rewrite->using_index_permalinks(), 'ep_mask' => EP_TAGS, ), 'post_format' => $post_format_base ? array( 'slug' => $post_format_base ) : false, ); } register_taxonomy( 'category', 'post', array( 'hierarchical' => true, 'query_var' => 'category_name', 'rewrite' => $rewrite['category'], 'public' => true, 'show_ui' => true, 'show_admin_column' => true, '_builtin' => true, 'capabilities' => array( 'manage_terms' => 'manage_categories', 'edit_terms' => 'edit_categories', 'delete_terms' => 'delete_categories', 'assign_terms' => 'assign_categories', ), 'show_in_rest' => true, 'rest_base' => 'categories', 'rest_controller_class' => 'WP_REST_Terms_Controller', ) ); register_taxonomy( 'post_tag', 'post', array( 'hierarchical' => false, 'query_var' => 'tag', 'rewrite' => $rewrite['post_tag'], 'public' => true, 'show_ui' => true, 'show_admin_column' => true, '_builtin' => true, 'capabilities' => array( 'manage_terms' => 'manage_post_tags', 'edit_terms' => 'edit_post_tags', 'delete_terms' => 'delete_post_tags', 'assign_terms' => 'assign_post_tags', ), 'show_in_rest' => true, 'rest_base' => 'tags', 'rest_controller_class' => 'WP_REST_Terms_Controller', ) ); register_taxonomy( 'nav_menu', 'nav_menu_item', array( 'public' => false, 'hierarchical' => false, 'labels' => array( 'name' => __( 'Navigation Menus' ), 'singular_name' => __( 'Navigation Menu' ), ), 'query_var' => false, 'rewrite' => false, 'show_ui' => false, '_builtin' => true, 'show_in_nav_menus' => false, 'capabilities' => array( 'manage_terms' => 'edit_theme_options', 'edit_terms' => 'edit_theme_options', 'delete_terms' => 'edit_theme_options', 'assign_terms' => 'edit_theme_options', ), 'show_in_rest' => true, 'rest_base' => 'menus', 'rest_controller_class' => 'WP_REST_Menus_Controller', ) ); register_taxonomy( 'link_category', 'link', array( 'hierarchical' => false, 'labels' => array( 'name' => __( 'Link Categories' ), 'singular_name' => __( 'Link Category' ), 'search_items' => __( 'Search Link Categories' ), 'popular_items' => null, 'all_items' => __( 'All Link Categories' ), 'edit_item' => __( 'Edit Link Category' ), 'update_item' => __( 'Update Link Category' ), 'add_new_item' => __( 'Add New Link Category' ), 'new_item_name' => __( 'New Link Category Name' ), 'separate_items_with_commas' => null, 'add_or_remove_items' => null, 'choose_from_most_used' => null, 'back_to_items' => __( '← Go to Link Categories' ), ), 'capabilities' => array( 'manage_terms' => 'manage_links', 'edit_terms' => 'manage_links', 'delete_terms' => 'manage_links', 'assign_terms' => 'manage_links', ), 'query_var' => false, 'rewrite' => false, 'public' => false, 'show_ui' => true, '_builtin' => true, ) ); register_taxonomy( 'post_format', 'post', array( 'public' => true, 'hierarchical' => false, 'labels' => array( 'name' => _x( 'Formats', 'post format' ), 'singular_name' => _x( 'Format', 'post format' ), ), 'query_var' => true, 'rewrite' => $rewrite['post_format'], 'show_ui' => false, '_builtin' => true, 'show_in_nav_menus' => current_theme_supports( 'post-formats' ), ) ); register_taxonomy( 'wp_theme', array( 'wp_template', 'wp_template_part', 'wp_global_styles' ), array( 'public' => false, 'hierarchical' => false, 'labels' => array( 'name' => __( 'Themes' ), 'singular_name' => __( 'Theme' ), ), 'query_var' => false, 'rewrite' => false, 'show_ui' => false, '_builtin' => true, 'show_in_nav_menus' => false, 'show_in_rest' => false, ) ); register_taxonomy( 'wp_template_part_area', array( 'wp_template_part' ), array( 'public' => false, 'hierarchical' => false, 'labels' => array( 'name' => __( 'Template Part Areas' ), 'singular_name' => __( 'Template Part Area' ), ), 'query_var' => false, 'rewrite' => false, 'show_ui' => false, '_builtin' => true, 'show_in_nav_menus' => false, 'show_in_rest' => false, ) ); register_taxonomy( 'wp_pattern_category', array( 'wp_block' ), array( 'public' => false, 'publicly_queryable' => false, 'hierarchical' => false, 'labels' => array( 'name' => _x( 'Pattern Categories', 'taxonomy general name' ), 'singular_name' => _x( 'Pattern Category', 'taxonomy singular name' ), ), 'query_var' => false, 'rewrite' => false, 'show_ui' => true, '_builtin' => true, 'show_in_nav_menus' => false, 'show_in_rest' => true, 'show_admin_column' => true, ) ); } /** * Retrieves a list of registered taxonomy names or objects. * * @since 3.0.0 * * @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies. * * @param array $args Optional. An array of `key => value` arguments to match against the taxonomy objects. * Default empty array. * @param string $output Optional. The type of output to return in the array. Accepts either taxonomy 'names' * or 'objects'. Default 'names'. * @param string $operator Optional. The logical operation to perform. Accepts 'and' or 'or'. 'or' means only * one element from the array needs to match; 'and' means all elements must match. * Default 'and'. * @return string[]|WP_Taxonomy[] An array of taxonomy names or objects. */ function get_taxonomies( $args = array(), $output = 'names', $operator = 'and' ) { global $wp_taxonomies; $field = ( 'names' === $output ) ? 'name' : false; return wp_filter_object_list( $wp_taxonomies, $args, $operator, $field ); } /** * Returns the names or objects of the taxonomies which are registered for the requested object or object type, * such as a post object or post type name. * * Example: * * $taxonomies = get_object_taxonomies( 'post' ); * * This results in: * * Array( 'category', 'post_tag' ) * * @since 2.3.0 * * @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies. * * @param string|string[]|WP_Post $object_type Name of the type of taxonomy object, or an object (row from posts). * @param string $output Optional. The type of output to return in the array. Accepts either * 'names' or 'objects'. Default 'names'. * @return string[]|WP_Taxonomy[] The names or objects of all taxonomies of `$object_type`. */ function get_object_taxonomies( $object_type, $output = 'names' ) { global $wp_taxonomies; if ( is_object( $object_type ) ) { if ( 'attachment' === $object_type->post_type ) { return get_attachment_taxonomies( $object_type, $output ); } $object_type = $object_type->post_type; } $object_type = (array) $object_type; $taxonomies = array(); foreach ( (array) $wp_taxonomies as $tax_name => $tax_obj ) { if ( array_intersect( $object_type, (array) $tax_obj->object_type ) ) { if ( 'names' === $output ) { $taxonomies[] = $tax_name; } else { $taxonomies[ $tax_name ] = $tax_obj; } } } return $taxonomies; } /** * Retrieves the taxonomy object of $taxonomy. * * The get_taxonomy function will first check that the parameter string given * is a taxonomy object and if it is, it will return it. * * @since 2.3.0 * * @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies. * * @param string $taxonomy Name of taxonomy object to return. * @return WP_Taxonomy|false The taxonomy object or false if $taxonomy doesn't exist. */ function get_taxonomy( $taxonomy ) { global $wp_taxonomies; if ( ! taxonomy_exists( $taxonomy ) ) { return false; } return $wp_taxonomies[ $taxonomy ]; } /** * Determines whether the taxonomy name exists. * * Formerly is_taxonomy(), introduced in 2.3.0. * * For more information on this and similar theme functions, check out * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ * Conditional Tags} article in the Theme Developer Handbook. * * @since 3.0.0 * * @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies. * * @param string $taxonomy Name of taxonomy object. * @return bool Whether the taxonomy exists. */ function taxonomy_exists( $taxonomy ) { global $wp_taxonomies; return is_string( $taxonomy ) && isset( $wp_taxonomies[ $taxonomy ] ); } /** * Determines whether the taxonomy object is hierarchical. * * Checks to make sure that the taxonomy is an object first. Then Gets the * object, and finally returns the hierarchical value in the object. * * A false return value might also mean that the taxonomy does not exist. * * For more information on this and similar theme functions, check out * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ * Conditional Tags} article in the Theme Developer Handbook. * * @since 2.3.0 * * @param string $taxonomy Name of taxonomy object. * @return bool Whether the taxonomy is hierarchical. */ function is_taxonomy_hierarchical( $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) { return false; } $taxonomy = get_taxonomy( $taxonomy ); return $taxonomy->hierarchical; } /** * Creates or modifies a taxonomy object. * * Note: Do not use before the {@see 'init'} hook. * * A simple function for creating or modifying a taxonomy object based on * the parameters given. If modifying an existing taxonomy object, note * that the `$object_type` value from the original registration will be * overwritten. * * @since 2.3.0 * @since 4.2.0 Introduced `show_in_quick_edit` argument. * @since 4.4.0 The `show_ui` argument is now enforced on the term editing screen. * @since 4.4.0 The `public` argument now controls whether the taxonomy can be queried on the front end. * @since 4.5.0 Introduced `publicly_queryable` argument. * @since 4.7.0 Introduced `show_in_rest`, 'rest_base' and 'rest_controller_class' * arguments to register the taxonomy in REST API. * @since 5.1.0 Introduced `meta_box_sanitize_cb` argument. * @since 5.4.0 Added the registered taxonomy object as a return value. * @since 5.5.0 Introduced `default_term` argument. * @since 5.9.0 Introduced `rest_namespace` argument. * * @global WP_Taxonomy[] $wp_taxonomies Registered taxonomies. * * @param string $taxonomy Taxonomy key. Must not exceed 32 characters and may only contain * lowercase alphanumeric characters, dashes, and underscores. See sanitize_key(). * @param array|string $object_type Object type or array of object types with which the taxonomy should be associated. * @param array|string $args { * Optional. Array or query string of arguments for registering a taxonomy. * * @type string[] $labels An array of labels for this taxonomy. By default, Tag labels are * used for non-hierarchical taxonomies, and Category labels are used * for hierarchical taxonomies. See accepted values in * get_taxonomy_labels(). Default empty array. * @type string $description A short descriptive summary of what the taxonomy is for. Default empty. * @type bool $public Whether a taxonomy is intended for use publicly either via * the admin interface or by front-end users. The default settings * of `$publicly_queryable`, `$show_ui`, and `$show_in_nav_menus` * are inherited from `$public`. * @type bool $publicly_queryable Whether the taxonomy is publicly queryable. * If not set, the default is inherited from `$public` * @type bool $hierarchical Whether the taxonomy is hierarchical. Default false. * @type bool $show_ui Whether to generate and allow a UI for managing terms in this taxonomy in * the admin. If not set, the default is inherited from `$public` * (default true). * @type bool $show_in_menu Whether to show the taxonomy in the admin menu. If true, the taxonomy is * shown as a submenu of the object type menu. If false, no menu is shown. * `$show_ui` must be true. If not set, default is inherited from `$show_ui` * (default true). * @type bool $show_in_nav_menus Makes this taxonomy available for selection in navigation menus. If not * set, the default is inherited from `$public` (default true). * @type bool $show_in_rest Whether to include the taxonomy in the REST API. Set this to true * for the taxonomy to be available in the block editor. * @type string $rest_base To change the base url of REST API route. Default is $taxonomy. * @type string $rest_namespace To change the namespace URL of REST API route. Default is wp/v2. * @type string $rest_controller_class REST API Controller class name. Default is 'WP_REST_Terms_Controller'. * @type bool $show_tagcloud Whether to list the taxonomy in the Tag Cloud Widget controls. If not set, * the default is inherited from `$show_ui` (default true). * @type bool $show_in_quick_edit Whether to show the taxonomy in the quick/bulk edit panel. It not set, * the default is inherited from `$show_ui` (default true). * @type bool $show_admin_column Whether to display a column for the taxonomy on its post type listing * screens. Default false. * @type bool|callable $meta_box_cb Provide a callback function for the meta box display. If not set, * post_categories_meta_box() is used for hierarchical taxonomies, and * post_tags_meta_box() is used for non-hierarchical. If false, no meta * box is shown. * @type callable $meta_box_sanitize_cb Callback function for sanitizing taxonomy data saved from a meta * box. If no callback is defined, an appropriate one is determined * based on the value of `$meta_box_cb`. * @type string[] $capabilities { * Array of capabilities for this taxonomy. * * @type string $manage_terms Default 'manage_categories'. * @type string $edit_terms Default 'manage_categories'. * @type string $delete_terms Default 'manage_categories'. * @type string $assign_terms Default 'edit_posts'. * } * @type bool|array $rewrite { * Triggers the handling of rewrites for this taxonomy. Default true, using $taxonomy as slug. To prevent * rewrite, set to false. To specify rewrite rules, an array can be passed with any of these keys: * * @type string $slug Customize the permastruct slug. Default `$taxonomy` key. * @type bool $with_front Should the permastruct be prepended with WP_Rewrite::$front. Default true. * @type bool $hierarchical Either hierarchical rewrite tag or not. Default false. * @type int $ep_mask Assign an endpoint mask. Default `EP_NONE`. * } * @type string|bool $query_var Sets the query var key for this taxonomy. Default `$taxonomy` key. If * false, a taxonomy cannot be loaded at `?{query_var}={term_slug}`. If a * string, the query `?{query_var}={term_slug}` will be valid. * @type callable $update_count_callback Works much like a hook, in that it will be called when the count is * updated. Default _update_post_term_count() for taxonomies attached * to post types, which confirms that the objects are published before * counting them. Default _update_generic_term_count() for taxonomies * attached to other object types, such as users. * @type string|array $default_term { * Default term to be used for the taxonomy. * * @type string $name Name of default term. * @type string $slug Slug for default term. Default empty. * @type string $description Description for default term. Default empty. * } * @type bool $sort Whether terms in this taxonomy should be sorted in the order they are * provided to `wp_set_object_terms()`. Default null which equates to false. * @type array $args Array of arguments to automatically use inside `wp_get_object_terms()` * for this taxonomy. * @type bool $_builtin This taxonomy is a "built-in" taxonomy. INTERNAL USE ONLY! * Default false. * } * @return WP_Taxonomy|WP_Error The registered taxonomy object on success, WP_Error object on failure. */ function register_taxonomy( $taxonomy, $object_type, $args = array() ) { global $wp_taxonomies; if ( ! is_array( $wp_taxonomies ) ) { $wp_taxonomies = array(); } $args = wp_parse_args( $args ); if ( empty( $taxonomy ) || strlen( $taxonomy ) > 32 ) { _doing_it_wrong( __FUNCTION__, __( 'Taxonomy names must be between 1 and 32 characters in length.' ), '4.2.0' ); return new WP_Error( 'taxonomy_length_invalid', __( 'Taxonomy names must be between 1 and 32 characters in length.' ) ); } $taxonomy_object = new WP_Taxonomy( $taxonomy, $object_type, $args ); $taxonomy_object->add_rewrite_rules(); $wp_taxonomies[ $taxonomy ] = $taxonomy_object; $taxonomy_object->add_hooks(); // Add default term. if ( ! empty( $taxonomy_object->default_term ) ) { $term = term_exists( $taxonomy_object->default_term['name'], $taxonomy ); if ( $term ) { update_option( 'default_term_' . $taxonomy_object->name, $term['term_id'] ); } else { $term = wp_insert_term( $taxonomy_object->default_term['name'], $taxonomy, array( 'slug' => sanitize_title( $taxonomy_object->default_term['slug'] ), 'description' => $taxonomy_object->default_term['description'], ) ); // Update `term_id` in options. if ( ! is_wp_error( $term ) ) { update_option( 'default_term_' . $taxonomy_object->name, $term['term_id'] ); } } } /** * Fires after a taxonomy is registered. * * @since 3.3.0 * * @param string $taxonomy Taxonomy slug. * @param array|string $object_type Object type or array of object types. * @param array $args Array of taxonomy registration arguments. */ do_action( 'registered_taxonomy', $taxonomy, $object_type, (array) $taxonomy_object ); /** * Fires after a specific taxonomy is registered. * * The dynamic portion of the filter name, `$taxonomy`, refers to the taxonomy key. * * Possible hook names include: * * - `registered_taxonomy_category` * - `registered_taxonomy_post_tag` * * @since 6.0.0 * * @param string $taxonomy Taxonomy slug. * @param array|string $object_type Object type or array of object types. * @param array $args Array of taxonomy registration arguments. */ do_action( "registered_taxonomy_{$taxonomy}", $taxonomy, $object_type, (array) $taxonomy_object ); return $taxonomy_object; } /** * Unregisters a taxonomy. * * Can not be used to unregister built-in taxonomies. * * @since 4.5.0 * * @global WP_Taxonomy[] $wp_taxonomies List of taxonomies. * * @param string $taxonomy Taxonomy name. * @return true|WP_Error True on success, WP_Error on failure or if the taxonomy doesn't exist. */ function unregister_taxonomy( $taxonomy ) { global $wp_taxonomies; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } $taxonomy_object = get_taxonomy( $taxonomy ); // Do not allow unregistering internal taxonomies. if ( $taxonomy_object->_builtin ) { return new WP_Error( 'invalid_taxonomy', __( 'Unregistering a built-in taxonomy is not allowed.' ) ); } $taxonomy_object->remove_rewrite_rules(); $taxonomy_object->remove_hooks(); // Remove the taxonomy. unset( $wp_taxonomies[ $taxonomy ] ); /** * Fires after a taxonomy is unregistered. * * @since 4.5.0 * * @param string $taxonomy Taxonomy name. */ do_action( 'unregistered_taxonomy', $taxonomy ); return true; } /** * Builds an object with all taxonomy labels out of a taxonomy object. * * @since 3.0.0 * @since 4.3.0 Added the `no_terms` label. * @since 4.4.0 Added the `items_list_navigation` and `items_list` labels. * @since 4.9.0 Added the `most_used` and `back_to_items` labels. * @since 5.7.0 Added the `filter_by_item` label. * @since 5.8.0 Added the `item_link` and `item_link_description` labels. * @since 5.9.0 Added the `name_field_description`, `slug_field_description`, * `parent_field_description`, and `desc_field_description` labels. * * @param WP_Taxonomy $tax Taxonomy object. * @return object { * Taxonomy labels object. The first default value is for non-hierarchical taxonomies * (like tags) and the second one is for hierarchical taxonomies (like categories). * * @type string $name General name for the taxonomy, usually plural. The same * as and overridden by `$tax->label`. Default 'Tags'/'Categories'. * @type string $singular_name Name for one object of this taxonomy. Default 'Tag'/'Category'. * @type string $search_items Default 'Search Tags'/'Search Categories'. * @type string $popular_items This label is only used for non-hierarchical taxonomies. * Default 'Popular Tags'. * @type string $all_items Default 'All Tags'/'All Categories'. * @type string $parent_item This label is only used for hierarchical taxonomies. Default * 'Parent Category'. * @type string $parent_item_colon The same as `parent_item`, but with colon `:` in the end. * @type string $name_field_description Description for the Name field on Edit Tags screen. * Default 'The name is how it appears on your site'. * @type string $slug_field_description Description for the Slug field on Edit Tags screen. * Default 'The “slug” is the URL-friendly version * of the name. It is usually all lowercase and contains * only letters, numbers, and hyphens'. * @type string $parent_field_description Description for the Parent field on Edit Tags screen. * Default 'Assign a parent term to create a hierarchy. * The term Jazz, for example, would be the parent * of Bebop and Big Band'. * @type string $desc_field_description Description for the Description field on Edit Tags screen. * Default 'The description is not prominent by default; * however, some themes may show it'. * @type string $edit_item Default 'Edit Tag'/'Edit Category'. * @type string $view_item Default 'View Tag'/'View Category'. * @type string $update_item Default 'Update Tag'/'Update Category'. * @type string $add_new_item Default 'Add New Tag'/'Add New Category'. * @type string $new_item_name Default 'New Tag Name'/'New Category Name'. * @type string $separate_items_with_commas This label is only used for non-hierarchical taxonomies. Default * 'Separate tags with commas', used in the meta box. * @type string $add_or_remove_items This label is only used for non-hierarchical taxonomies. Default * 'Add or remove tags', used in the meta box when JavaScript * is disabled. * @type string $choose_from_most_used This label is only used on non-hierarchical taxonomies. Default * 'Choose from the most used tags', used in the meta box. * @type string $not_found Default 'No tags found'/'No categories found', used in * the meta box and taxonomy list table. * @type string $no_terms Default 'No tags'/'No categories', used in the posts and media * list tables. * @type string $filter_by_item This label is only used for hierarchical taxonomies. Default * 'Filter by category', used in the posts list table. * @type string $items_list_navigation Label for the table pagination hidden heading. * @type string $items_list Label for the table hidden heading. * @type string $most_used Title for the Most Used tab. Default 'Most Used'. * @type string $back_to_items Label displayed after a term has been updated. * @type string $item_link Used in the block editor. Title for a navigation link block variation. * Default 'Tag Link'/'Category Link'. * @type string $item_link_description Used in the block editor. Description for a navigation link block * variation. Default 'A link to a tag'/'A link to a category'. * } */ function get_taxonomy_labels( $tax ) { $tax->labels = (array) $tax->labels; if ( isset( $tax->helps ) && empty( $tax->labels['separate_items_with_commas'] ) ) { $tax->labels['separate_items_with_commas'] = $tax->helps; } if ( isset( $tax->no_tagcloud ) && empty( $tax->labels['not_found'] ) ) { $tax->labels['not_found'] = $tax->no_tagcloud; } $nohier_vs_hier_defaults = WP_Taxonomy::get_default_labels(); $nohier_vs_hier_defaults['menu_name'] = $nohier_vs_hier_defaults['name']; $labels = _get_custom_object_labels( $tax, $nohier_vs_hier_defaults ); $taxonomy = $tax->name; $default_labels = clone $labels; /** * Filters the labels of a specific taxonomy. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * Possible hook names include: * * - `taxonomy_labels_category` * - `taxonomy_labels_post_tag` * * @since 4.4.0 * * @see get_taxonomy_labels() for the full list of taxonomy labels. * * @param object $labels Object with labels for the taxonomy as member variables. */ $labels = apply_filters( "taxonomy_labels_{$taxonomy}", $labels ); // Ensure that the filtered labels contain all required default values. $labels = (object) array_merge( (array) $default_labels, (array) $labels ); return $labels; } /** * Adds an already registered taxonomy to an object type. * * @since 3.0.0 * * @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies. * * @param string $taxonomy Name of taxonomy object. * @param string $object_type Name of the object type. * @return bool True if successful, false if not. */ function register_taxonomy_for_object_type( $taxonomy, $object_type ) { global $wp_taxonomies; if ( ! isset( $wp_taxonomies[ $taxonomy ] ) ) { return false; } if ( ! get_post_type_object( $object_type ) ) { return false; } if ( ! in_array( $object_type, $wp_taxonomies[ $taxonomy ]->object_type, true ) ) { $wp_taxonomies[ $taxonomy ]->object_type[] = $object_type; } // Filter out empties. $wp_taxonomies[ $taxonomy ]->object_type = array_filter( $wp_taxonomies[ $taxonomy ]->object_type ); /** * Fires after a taxonomy is registered for an object type. * * @since 5.1.0 * * @param string $taxonomy Taxonomy name. * @param string $object_type Name of the object type. */ do_action( 'registered_taxonomy_for_object_type', $taxonomy, $object_type ); return true; } /** * Removes an already registered taxonomy from an object type. * * @since 3.7.0 * * @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies. * * @param string $taxonomy Name of taxonomy object. * @param string $object_type Name of the object type. * @return bool True if successful, false if not. */ function unregister_taxonomy_for_object_type( $taxonomy, $object_type ) { global $wp_taxonomies; if ( ! isset( $wp_taxonomies[ $taxonomy ] ) ) { return false; } if ( ! get_post_type_object( $object_type ) ) { return false; } $key = array_search( $object_type, $wp_taxonomies[ $taxonomy ]->object_type, true ); if ( false === $key ) { return false; } unset( $wp_taxonomies[ $taxonomy ]->object_type[ $key ] ); /** * Fires after a taxonomy is unregistered for an object type. * * @since 5.1.0 * * @param string $taxonomy Taxonomy name. * @param string $object_type Name of the object type. */ do_action( 'unregistered_taxonomy_for_object_type', $taxonomy, $object_type ); return true; } // // Term API. // /** * Retrieves object IDs of valid taxonomy and term. * * The strings of `$taxonomies` must exist before this function will continue. * On failure of finding a valid taxonomy, it will return a WP_Error. * * The `$terms` aren't checked the same as `$taxonomies`, but still need to exist * for object IDs to be returned. * * It is possible to change the order that object IDs are returned by using `$args` * with either ASC or DESC array. The value should be in the key named 'order'. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int|int[] $term_ids Term ID or array of term IDs of terms that will be used. * @param string|string[] $taxonomies String of taxonomy name or Array of string values of taxonomy names. * @param array|string $args { * Change the order of the object IDs. * * @type string $order Order to retrieve terms. Accepts 'ASC' or 'DESC'. Default 'ASC'. * } * @return string[]|WP_Error An array of object IDs as numeric strings on success, * WP_Error if the taxonomy does not exist. */ function get_objects_in_term( $term_ids, $taxonomies, $args = array() ) { global $wpdb; if ( ! is_array( $term_ids ) ) { $term_ids = array( $term_ids ); } if ( ! is_array( $taxonomies ) ) { $taxonomies = array( $taxonomies ); } foreach ( (array) $taxonomies as $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } } $defaults = array( 'order' => 'ASC' ); $args = wp_parse_args( $args, $defaults ); $order = ( 'desc' === strtolower( $args['order'] ) ) ? 'DESC' : 'ASC'; $term_ids = array_map( 'intval', $term_ids ); $taxonomies = "'" . implode( "', '", array_map( 'esc_sql', $taxonomies ) ) . "'"; $term_ids = "'" . implode( "', '", $term_ids ) . "'"; $sql = "SELECT tr.object_id FROM $wpdb->term_relationships AS tr INNER JOIN $wpdb->term_taxonomy AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy IN ($taxonomies) AND tt.term_id IN ($term_ids) ORDER BY tr.object_id $order"; $last_changed = wp_cache_get_last_changed( 'terms' ); $cache_key = 'get_objects_in_term:' . md5( $sql ) . ":$last_changed"; $cache = wp_cache_get( $cache_key, 'term-queries' ); if ( false === $cache ) { $object_ids = $wpdb->get_col( $sql ); wp_cache_set( $cache_key, $object_ids, 'term-queries' ); } else { $object_ids = (array) $cache; } if ( ! $object_ids ) { return array(); } return $object_ids; } /** * Given a taxonomy query, generates SQL to be appended to a main query. * * @since 3.1.0 * * @see WP_Tax_Query * * @param array $tax_query A compact tax query * @param string $primary_table * @param string $primary_id_column * @return string[] */ function get_tax_sql( $tax_query, $primary_table, $primary_id_column ) { $tax_query_obj = new WP_Tax_Query( $tax_query ); return $tax_query_obj->get_sql( $primary_table, $primary_id_column ); } /** * Gets all term data from database by term ID. * * The usage of the get_term function is to apply filters to a term object. It * is possible to get a term object from the database before applying the * filters. * * $term ID must be part of $taxonomy, to get from the database. Failure, might * be able to be captured by the hooks. Failure would be the same value as $wpdb * returns for the get_row method. * * There are two hooks, one is specifically for each term, named 'get_term', and * the second is for the taxonomy name, 'term_$taxonomy'. Both hooks gets the * term object, and the taxonomy name as parameters. Both hooks are expected to * return a term object. * * {@see 'get_term'} hook - Takes two parameters the term Object and the taxonomy name. * Must return term object. Used in get_term() as a catch-all filter for every * $term. * * {@see 'get_$taxonomy'} hook - Takes two parameters the term Object and the taxonomy * name. Must return term object. $taxonomy will be the taxonomy name, so for * example, if 'category', it would be 'get_category' as the filter name. Useful * for custom taxonomies or plugging into default taxonomies. * * @todo Better formatting for DocBlock * * @since 2.3.0 * @since 4.4.0 Converted to return a WP_Term object if `$output` is `OBJECT`. * The `$taxonomy` parameter was made optional. * * @see sanitize_term_field() The $context param lists the available values for get_term_by() $filter param. * * @param int|WP_Term|object $term If integer, term data will be fetched from the database, * or from the cache if available. * If stdClass object (as in the results of a database query), * will apply filters and return a `WP_Term` object with the `$term` data. * If `WP_Term`, will return `$term`. * @param string $taxonomy Optional. Taxonomy name that `$term` is part of. * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which * correspond to a WP_Term object, an associative array, or a numeric array, * respectively. Default OBJECT. * @param string $filter Optional. How to sanitize term fields. Default 'raw'. * @return WP_Term|array|WP_Error|null WP_Term instance (or array) on success, depending on the `$output` value. * WP_Error if `$taxonomy` does not exist. Null for miscellaneous failure. */ function get_term( $term, $taxonomy = '', $output = OBJECT, $filter = 'raw' ) { if ( empty( $term ) ) { return new WP_Error( 'invalid_term', __( 'Empty Term.' ) ); } if ( $taxonomy && ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } if ( $term instanceof WP_Term ) { $_term = $term; } elseif ( is_object( $term ) ) { if ( empty( $term->filter ) || 'raw' === $term->filter ) { $_term = sanitize_term( $term, $taxonomy, 'raw' ); $_term = new WP_Term( $_term ); } else { $_term = WP_Term::get_instance( $term->term_id ); } } else { $_term = WP_Term::get_instance( $term, $taxonomy ); } if ( is_wp_error( $_term ) ) { return $_term; } elseif ( ! $_term ) { return null; } // Ensure for filters that this is not empty. $taxonomy = $_term->taxonomy; $old_term = $_term; /** * Filters a taxonomy term object. * * The {@see 'get_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * @since 4.4.0 `$_term` is now a `WP_Term` object. * * @param WP_Term $_term Term object. * @param string $taxonomy The taxonomy slug. */ $_term = apply_filters( 'get_term', $_term, $taxonomy ); /** * Filters a taxonomy term object. * * The dynamic portion of the hook name, `$taxonomy`, refers * to the slug of the term's taxonomy. * * Possible hook names include: * * - `get_category` * - `get_post_tag` * * @since 2.3.0 * @since 4.4.0 `$_term` is now a `WP_Term` object. * * @param WP_Term $_term Term object. * @param string $taxonomy The taxonomy slug. */ $_term = apply_filters( "get_{$taxonomy}", $_term, $taxonomy ); // Bail if a filter callback has changed the type of the `$_term` object. if ( ! ( $_term instanceof WP_Term ) ) { return $_term; } // Sanitize term, according to the specified filter. if ( $_term !== $old_term || $_term->filter !== $filter ) { $_term->filter( $filter ); } if ( ARRAY_A === $output ) { return $_term->to_array(); } elseif ( ARRAY_N === $output ) { return array_values( $_term->to_array() ); } return $_term; } /** * Gets all term data from database by term field and data. * * Warning: $value is not escaped for 'name' $field. You must do it yourself, if * required. * * The default $field is 'id', therefore it is possible to also use null for * field, but not recommended that you do so. * * If $value does not exist, the return value will be false. If $taxonomy exists * and $field and $value combinations exist, the term will be returned. * * This function will always return the first term that matches the `$field`- * `$value`-`$taxonomy` combination specified in the parameters. If your query * is likely to match more than one term (as is likely to be the case when * `$field` is 'name', for example), consider using get_terms() instead; that * way, you will get all matching terms, and can provide your own logic for * deciding which one was intended. * * @todo Better formatting for DocBlock. * * @since 2.3.0 * @since 4.4.0 `$taxonomy` is optional if `$field` is 'term_taxonomy_id'. Converted to return * a WP_Term object if `$output` is `OBJECT`. * @since 5.5.0 Added 'ID' as an alias of 'id' for the `$field` parameter. * * @see sanitize_term_field() The $context param lists the available values for get_term_by() $filter param. * * @param string $field Either 'slug', 'name', 'term_id' (or 'id', 'ID'), or 'term_taxonomy_id'. * @param string|int $value Search for this term value. * @param string $taxonomy Taxonomy name. Optional, if `$field` is 'term_taxonomy_id'. * @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which * correspond to a WP_Term object, an associative array, or a numeric array, * respectively. Default OBJECT. * @param string $filter Optional. How to sanitize term fields. Default 'raw'. * @return WP_Term|array|false WP_Term instance (or array) on success, depending on the `$output` value. * False if `$taxonomy` does not exist or `$term` was not found. */ function get_term_by( $field, $value, $taxonomy = '', $output = OBJECT, $filter = 'raw' ) { // 'term_taxonomy_id' lookups don't require taxonomy checks. if ( 'term_taxonomy_id' !== $field && ! taxonomy_exists( $taxonomy ) ) { return false; } // No need to perform a query for empty 'slug' or 'name'. if ( 'slug' === $field || 'name' === $field ) { $value = (string) $value; if ( 0 === strlen( $value ) ) { return false; } } if ( 'id' === $field || 'ID' === $field || 'term_id' === $field ) { $term = get_term( (int) $value, $taxonomy, $output, $filter ); if ( is_wp_error( $term ) || null === $term ) { $term = false; } return $term; } $args = array( 'get' => 'all', 'number' => 1, 'taxonomy' => $taxonomy, 'update_term_meta_cache' => false, 'orderby' => 'none', 'suppress_filter' => true, ); switch ( $field ) { case 'slug': $args['slug'] = $value; break; case 'name': $args['name'] = $value; break; case 'term_taxonomy_id': $args['term_taxonomy_id'] = $value; unset( $args['taxonomy'] ); break; default: return false; } $terms = get_terms( $args ); if ( is_wp_error( $terms ) || empty( $terms ) ) { return false; } $term = array_shift( $terms ); // In the case of 'term_taxonomy_id', override the provided `$taxonomy` with whatever we find in the DB. if ( 'term_taxonomy_id' === $field ) { $taxonomy = $term->taxonomy; } return get_term( $term, $taxonomy, $output, $filter ); } /** * Merges all term children into a single array of their IDs. * * This recursive function will merge all of the children of $term into the same * array of term IDs. Only useful for taxonomies which are hierarchical. * * Will return an empty array if $term does not exist in $taxonomy. * * @since 2.3.0 * * @param int $term_id ID of term to get children. * @param string $taxonomy Taxonomy name. * @return array|WP_Error List of term IDs. WP_Error returned if `$taxonomy` does not exist. */ function get_term_children( $term_id, $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } $term_id = (int) $term_id; $terms = _get_term_hierarchy( $taxonomy ); if ( ! isset( $terms[ $term_id ] ) ) { return array(); } $children = $terms[ $term_id ]; foreach ( (array) $terms[ $term_id ] as $child ) { if ( $term_id === $child ) { continue; } if ( isset( $terms[ $child ] ) ) { $children = array_merge( $children, get_term_children( $child, $taxonomy ) ); } } return $children; } /** * Gets sanitized term field. * * The function is for contextual reasons and for simplicity of usage. * * @since 2.3.0 * @since 4.4.0 The `$taxonomy` parameter was made optional. `$term` can also now accept a WP_Term object. * * @see sanitize_term_field() * * @param string $field Term field to fetch. * @param int|WP_Term $term Term ID or object. * @param string $taxonomy Optional. Taxonomy name. Default empty. * @param string $context Optional. How to sanitize term fields. Look at sanitize_term_field() for available options. * Default 'display'. * @return string|int|null|WP_Error Will return an empty string if $term is not an object or if $field is not set in $term. */ function get_term_field( $field, $term, $taxonomy = '', $context = 'display' ) { $term = get_term( $term, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } if ( ! is_object( $term ) ) { return ''; } if ( ! isset( $term->$field ) ) { return ''; } return sanitize_term_field( $field, $term->$field, $term->term_id, $term->taxonomy, $context ); } /** * Sanitizes term for editing. * * Return value is sanitize_term() and usage is for sanitizing the term for * editing. Function is for contextual and simplicity. * * @since 2.3.0 * * @param int|object $id Term ID or object. * @param string $taxonomy Taxonomy name. * @return string|int|null|WP_Error Will return empty string if $term is not an object. */ function get_term_to_edit( $id, $taxonomy ) { $term = get_term( $id, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } if ( ! is_object( $term ) ) { return ''; } return sanitize_term( $term, $taxonomy, 'edit' ); } /** * Retrieves the terms in a given taxonomy or list of taxonomies. * * You can fully inject any customizations to the query before it is sent, as * well as control the output with a filter. * * The return type varies depending on the value passed to `$args['fields']`. See * WP_Term_Query::get_terms() for details. In all cases, a `WP_Error` object will * be returned if an invalid taxonomy is requested. * * The {@see 'get_terms'} filter will be called when the cache has the term and will * pass the found term along with the array of $taxonomies and array of $args. * This filter is also called before the array of terms is passed and will pass * the array of terms, along with the $taxonomies and $args. * * The {@see 'list_terms_exclusions'} filter passes the compiled exclusions along with * the $args. * * The {@see 'get_terms_orderby'} filter passes the `ORDER BY` clause for the query * along with the $args array. * * Taxonomy or an array of taxonomies should be passed via the 'taxonomy' argument * in the `$args` array: * * $terms = get_terms( array( * 'taxonomy' => 'post_tag', * 'hide_empty' => false, * ) ); * * Prior to 4.5.0, taxonomy was passed as the first parameter of `get_terms()`. * * @since 2.3.0 * @since 4.2.0 Introduced 'name' and 'childless' parameters. * @since 4.4.0 Introduced the ability to pass 'term_id' as an alias of 'id' for the `orderby` parameter. * Introduced the 'meta_query' and 'update_term_meta_cache' parameters. Converted to return * a list of WP_Term objects. * @since 4.5.0 Changed the function signature so that the `$args` array can be provided as the first parameter. * Introduced 'meta_key' and 'meta_value' parameters. Introduced the ability to order results by metadata. * @since 4.8.0 Introduced 'suppress_filter' parameter. * * @internal The `$deprecated` parameter is parsed for backward compatibility only. * * @param array|string $args Optional. Array or string of arguments. See WP_Term_Query::__construct() * for information on accepted arguments. Default empty array. * @param array|string $deprecated Optional. Argument array, when using the legacy function parameter format. * If present, this parameter will be interpreted as `$args`, and the first * function parameter will be parsed as a taxonomy or array of taxonomies. * Default empty. * @return WP_Term[]|int[]|string[]|string|WP_Error Array of terms, a count thereof as a numeric string, * or WP_Error if any of the taxonomies do not exist. * See the function description for more information. */ function get_terms( $args = array(), $deprecated = '' ) { $term_query = new WP_Term_Query(); $defaults = array( 'suppress_filter' => false, ); /* * Legacy argument format ($taxonomy, $args) takes precedence. * * We detect legacy argument format by checking if * (a) a second non-empty parameter is passed, or * (b) the first parameter shares no keys with the default array (ie, it's a list of taxonomies) */ $_args = wp_parse_args( $args ); $key_intersect = array_intersect_key( $term_query->query_var_defaults, (array) $_args ); $do_legacy_args = $deprecated || empty( $key_intersect ); if ( $do_legacy_args ) { $taxonomies = (array) $args; $args = wp_parse_args( $deprecated, $defaults ); $args['taxonomy'] = $taxonomies; } else { $args = wp_parse_args( $args, $defaults ); if ( isset( $args['taxonomy'] ) && null !== $args['taxonomy'] ) { $args['taxonomy'] = (array) $args['taxonomy']; } } if ( ! empty( $args['taxonomy'] ) ) { foreach ( $args['taxonomy'] as $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } } } // Don't pass suppress_filter to WP_Term_Query. $suppress_filter = $args['suppress_filter']; unset( $args['suppress_filter'] ); $terms = $term_query->query( $args ); // Count queries are not filtered, for legacy reasons. if ( ! is_array( $terms ) ) { return $terms; } if ( $suppress_filter ) { return $terms; } /** * Filters the found terms. * * @since 2.3.0 * @since 4.6.0 Added the `$term_query` parameter. * * @param array $terms Array of found terms. * @param array|null $taxonomies An array of taxonomies if known. * @param array $args An array of get_terms() arguments. * @param WP_Term_Query $term_query The WP_Term_Query object. */ return apply_filters( 'get_terms', $terms, $term_query->query_vars['taxonomy'], $term_query->query_vars, $term_query ); } /** * Adds metadata to a term. * * @since 4.4.0 * * @param int $term_id Term ID. * @param string $meta_key Metadata name. * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. * @param bool $unique Optional. Whether the same key should not be added. * Default false. * @return int|false|WP_Error Meta ID on success, false on failure. * WP_Error when term_id is ambiguous between taxonomies. */ function add_term_meta( $term_id, $meta_key, $meta_value, $unique = false ) { if ( wp_term_is_shared( $term_id ) ) { return new WP_Error( 'ambiguous_term_id', __( 'Term meta cannot be added to terms that are shared between taxonomies.' ), $term_id ); } return add_metadata( 'term', $term_id, $meta_key, $meta_value, $unique ); } /** * Removes metadata matching criteria from a term. * * @since 4.4.0 * * @param int $term_id Term ID. * @param string $meta_key Metadata name. * @param mixed $meta_value Optional. Metadata value. If provided, * rows will only be removed that match the value. * Must be serializable if non-scalar. Default empty. * @return bool True on success, false on failure. */ function delete_term_meta( $term_id, $meta_key, $meta_value = '' ) { return delete_metadata( 'term', $term_id, $meta_key, $meta_value ); } /** * Retrieves metadata for a term. * * @since 4.4.0 * * @param int $term_id Term ID. * @param string $key Optional. The meta key to retrieve. By default, * returns data for all keys. Default empty. * @param bool $single Optional. Whether to return a single value. * This parameter has no effect if `$key` is not specified. * Default false. * @return mixed An array of values if `$single` is false. * The value of the meta field if `$single` is true. * False for an invalid `$term_id` (non-numeric, zero, or negative value). * An empty string if a valid but non-existing term ID is passed. */ function get_term_meta( $term_id, $key = '', $single = false ) { return get_metadata( 'term', $term_id, $key, $single ); } /** * Updates term metadata. * * Use the `$prev_value` parameter to differentiate between meta fields with the same key and term ID. * * If the meta field for the term does not exist, it will be added. * * @since 4.4.0 * * @param int $term_id Term ID. * @param string $meta_key Metadata key. * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. * @param mixed $prev_value Optional. Previous value to check before updating. * If specified, only update existing metadata entries with * this value. Otherwise, update all entries. Default empty. * @return int|bool|WP_Error Meta ID if the key didn't exist. true on successful update, * false on failure or if the value passed to the function * is the same as the one that is already in the database. * WP_Error when term_id is ambiguous between taxonomies. */ function update_term_meta( $term_id, $meta_key, $meta_value, $prev_value = '' ) { if ( wp_term_is_shared( $term_id ) ) { return new WP_Error( 'ambiguous_term_id', __( 'Term meta cannot be added to terms that are shared between taxonomies.' ), $term_id ); } return update_metadata( 'term', $term_id, $meta_key, $meta_value, $prev_value ); } /** * Updates metadata cache for list of term IDs. * * Performs SQL query to retrieve all metadata for the terms matching `$term_ids` and stores them in the cache. * Subsequent calls to `get_term_meta()` will not need to query the database. * * @since 4.4.0 * * @param array $term_ids List of term IDs. * @return array|false An array of metadata on success, false if there is nothing to update. */ function update_termmeta_cache( $term_ids ) { return update_meta_cache( 'term', $term_ids ); } /** * Queue term meta for lazy-loading. * * @since 6.3.0 * * @param array $term_ids List of term IDs. */ function wp_lazyload_term_meta( array $term_ids ) { if ( empty( $term_ids ) ) { return; } $lazyloader = wp_metadata_lazyloader(); $lazyloader->queue_objects( 'term', $term_ids ); } /** * Gets all meta data, including meta IDs, for the given term ID. * * @since 4.9.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id Term ID. * @return array|false Array with meta data, or false when the meta table is not installed. */ function has_term_meta( $term_id ) { $check = wp_check_term_meta_support_prefilter( null ); if ( null !== $check ) { return $check; } global $wpdb; return $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value, meta_id, term_id FROM $wpdb->termmeta WHERE term_id = %d ORDER BY meta_key,meta_id", $term_id ), ARRAY_A ); } /** * Registers a meta key for terms. * * @since 4.9.8 * * @param string $taxonomy Taxonomy to register a meta key for. Pass an empty string * to register the meta key across all existing taxonomies. * @param string $meta_key The meta key to register. * @param array $args Data used to describe the meta key when registered. See * {@see register_meta()} for a list of supported arguments. * @return bool True if the meta key was successfully registered, false if not. */ function register_term_meta( $taxonomy, $meta_key, array $args ) { $args['object_subtype'] = $taxonomy; return register_meta( 'term', $meta_key, $args ); } /** * Unregisters a meta key for terms. * * @since 4.9.8 * * @param string $taxonomy Taxonomy the meta key is currently registered for. Pass * an empty string if the meta key is registered across all * existing taxonomies. * @param string $meta_key The meta key to unregister. * @return bool True on success, false if the meta key was not previously registered. */ function unregister_term_meta( $taxonomy, $meta_key ) { return unregister_meta_key( 'term', $meta_key, $taxonomy ); } /** * Determines whether a taxonomy term exists. * * Formerly is_term(), introduced in 2.3.0. * * For more information on this and similar theme functions, check out * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ * Conditional Tags} article in the Theme Developer Handbook. * * @since 3.0.0 * @since 6.0.0 Converted to use `get_terms()`. * * @global bool $_wp_suspend_cache_invalidation * * @param int|string $term The term to check. Accepts term ID, slug, or name. * @param string $taxonomy Optional. The taxonomy name to use. * @param int $parent_term Optional. ID of parent term under which to confine the exists search. * @return mixed Returns null if the term does not exist. * Returns the term ID if no taxonomy is specified and the term ID exists. * Returns an array of the term ID and the term taxonomy ID if the taxonomy is specified and the pairing exists. * Returns 0 if term ID 0 is passed to the function. */ function term_exists( $term, $taxonomy = '', $parent_term = null ) { global $_wp_suspend_cache_invalidation; if ( null === $term ) { return null; } $defaults = array( 'get' => 'all', 'fields' => 'ids', 'number' => 1, 'update_term_meta_cache' => false, 'order' => 'ASC', 'orderby' => 'term_id', 'suppress_filter' => true, ); // Ensure that while importing, queries are not cached. if ( ! empty( $_wp_suspend_cache_invalidation ) ) { $defaults['cache_results'] = false; } if ( ! empty( $taxonomy ) ) { $defaults['taxonomy'] = $taxonomy; $defaults['fields'] = 'all'; } /** * Filters default query arguments for checking if a term exists. * * @since 6.0.0 * * @param array $defaults An array of arguments passed to get_terms(). * @param int|string $term The term to check. Accepts term ID, slug, or name. * @param string $taxonomy The taxonomy name to use. An empty string indicates * the search is against all taxonomies. * @param int|null $parent_term ID of parent term under which to confine the exists search. * Null indicates the search is unconfined. */ $defaults = apply_filters( 'term_exists_default_query_args', $defaults, $term, $taxonomy, $parent_term ); if ( is_int( $term ) ) { if ( 0 === $term ) { return 0; } $args = wp_parse_args( array( 'include' => array( $term ) ), $defaults ); $terms = get_terms( $args ); } else { $term = trim( wp_unslash( $term ) ); if ( '' === $term ) { return null; } if ( ! empty( $taxonomy ) && is_numeric( $parent_term ) ) { $defaults['parent'] = (int) $parent_term; } $args = wp_parse_args( array( 'slug' => sanitize_title( $term ) ), $defaults ); $terms = get_terms( $args ); if ( empty( $terms ) || is_wp_error( $terms ) ) { $args = wp_parse_args( array( 'name' => $term ), $defaults ); $terms = get_terms( $args ); } } if ( empty( $terms ) || is_wp_error( $terms ) ) { return null; } $_term = array_shift( $terms ); if ( ! empty( $taxonomy ) ) { return array( 'term_id' => (string) $_term->term_id, 'term_taxonomy_id' => (string) $_term->term_taxonomy_id, ); } return (string) $_term; } /** * Checks if a term is an ancestor of another term. * * You can use either an ID or the term object for both parameters. * * @since 3.4.0 * * @param int|object $term1 ID or object to check if this is the parent term. * @param int|object $term2 The child term. * @param string $taxonomy Taxonomy name that $term1 and `$term2` belong to. * @return bool Whether `$term2` is a child of `$term1`. */ function term_is_ancestor_of( $term1, $term2, $taxonomy ) { if ( ! isset( $term1->term_id ) ) { $term1 = get_term( $term1, $taxonomy ); } if ( ! isset( $term2->parent ) ) { $term2 = get_term( $term2, $taxonomy ); } if ( empty( $term1->term_id ) || empty( $term2->parent ) ) { return false; } if ( $term2->parent === $term1->term_id ) { return true; } return term_is_ancestor_of( $term1, get_term( $term2->parent, $taxonomy ), $taxonomy ); } /** * Sanitizes all term fields. * * Relies on sanitize_term_field() to sanitize the term. The difference is that * this function will sanitize **all** fields. The context is based * on sanitize_term_field(). * * The `$term` is expected to be either an array or an object. * * @since 2.3.0 * * @param array|object $term The term to check. * @param string $taxonomy The taxonomy name to use. * @param string $context Optional. Context in which to sanitize the term. * Accepts 'raw', 'edit', 'db', 'display', 'rss', * 'attribute', or 'js'. Default 'display'. * @return array|object Term with all fields sanitized. */ function sanitize_term( $term, $taxonomy, $context = 'display' ) { $fields = array( 'term_id', 'name', 'description', 'slug', 'count', 'parent', 'term_group', 'term_taxonomy_id', 'object_id' ); $do_object = is_object( $term ); $term_id = $do_object ? $term->term_id : ( isset( $term['term_id'] ) ? $term['term_id'] : 0 ); foreach ( (array) $fields as $field ) { if ( $do_object ) { if ( isset( $term->$field ) ) { $term->$field = sanitize_term_field( $field, $term->$field, $term_id, $taxonomy, $context ); } } else { if ( isset( $term[ $field ] ) ) { $term[ $field ] = sanitize_term_field( $field, $term[ $field ], $term_id, $taxonomy, $context ); } } } if ( $do_object ) { $term->filter = $context; } else { $term['filter'] = $context; } return $term; } /** * Sanitizes the field value in the term based on the context. * * Passing a term field value through the function should be assumed to have * cleansed the value for whatever context the term field is going to be used. * * If no context or an unsupported context is given, then default filters will * be applied. * * There are enough filters for each context to support a custom filtering * without creating your own filter function. Simply create a function that * hooks into the filter you need. * * @since 2.3.0 * * @param string $field Term field to sanitize. * @param string $value Search for this term value. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy name. * @param string $context Context in which to sanitize the term field. * Accepts 'raw', 'edit', 'db', 'display', 'rss', * 'attribute', or 'js'. Default 'display'. * @return mixed Sanitized field. */ function sanitize_term_field( $field, $value, $term_id, $taxonomy, $context ) { $int_fields = array( 'parent', 'term_id', 'count', 'term_group', 'term_taxonomy_id', 'object_id' ); if ( in_array( $field, $int_fields, true ) ) { $value = (int) $value; if ( $value < 0 ) { $value = 0; } } $context = strtolower( $context ); if ( 'raw' === $context ) { return $value; } if ( 'edit' === $context ) { /** * Filters a term field to edit before it is sanitized. * * The dynamic portion of the hook name, `$field`, refers to the term field. * * @since 2.3.0 * * @param mixed $value Value of the term field. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. */ $value = apply_filters( "edit_term_{$field}", $value, $term_id, $taxonomy ); /** * Filters the taxonomy field to edit before it is sanitized. * * The dynamic portions of the filter name, `$taxonomy` and `$field`, refer * to the taxonomy slug and taxonomy field, respectively. * * @since 2.3.0 * * @param mixed $value Value of the taxonomy field to edit. * @param int $term_id Term ID. */ $value = apply_filters( "edit_{$taxonomy}_{$field}", $value, $term_id ); if ( 'description' === $field ) { $value = esc_html( $value ); // textarea_escaped } else { $value = esc_attr( $value ); } } elseif ( 'db' === $context ) { /** * Filters a term field value before it is sanitized. * * The dynamic portion of the hook name, `$field`, refers to the term field. * * @since 2.3.0 * * @param mixed $value Value of the term field. * @param string $taxonomy Taxonomy slug. */ $value = apply_filters( "pre_term_{$field}", $value, $taxonomy ); /** * Filters a taxonomy field before it is sanitized. * * The dynamic portions of the filter name, `$taxonomy` and `$field`, refer * to the taxonomy slug and field name, respectively. * * @since 2.3.0 * * @param mixed $value Value of the taxonomy field. */ $value = apply_filters( "pre_{$taxonomy}_{$field}", $value ); // Back compat filters. if ( 'slug' === $field ) { /** * Filters the category nicename before it is sanitized. * * Use the {@see 'pre_$taxonomy_$field'} hook instead. * * @since 2.0.3 * * @param string $value The category nicename. */ $value = apply_filters( 'pre_category_nicename', $value ); } } elseif ( 'rss' === $context ) { /** * Filters the term field for use in RSS. * * The dynamic portion of the hook name, `$field`, refers to the term field. * * @since 2.3.0 * * @param mixed $value Value of the term field. * @param string $taxonomy Taxonomy slug. */ $value = apply_filters( "term_{$field}_rss", $value, $taxonomy ); /** * Filters the taxonomy field for use in RSS. * * The dynamic portions of the hook name, `$taxonomy`, and `$field`, refer * to the taxonomy slug and field name, respectively. * * @since 2.3.0 * * @param mixed $value Value of the taxonomy field. */ $value = apply_filters( "{$taxonomy}_{$field}_rss", $value ); } else { // Use display filters by default. /** * Filters the term field sanitized for display. * * The dynamic portion of the hook name, `$field`, refers to the term field name. * * @since 2.3.0 * * @param mixed $value Value of the term field. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param string $context Context to retrieve the term field value. */ $value = apply_filters( "term_{$field}", $value, $term_id, $taxonomy, $context ); /** * Filters the taxonomy field sanitized for display. * * The dynamic portions of the filter name, `$taxonomy`, and `$field`, refer * to the taxonomy slug and taxonomy field, respectively. * * @since 2.3.0 * * @param mixed $value Value of the taxonomy field. * @param int $term_id Term ID. * @param string $context Context to retrieve the taxonomy field value. */ $value = apply_filters( "{$taxonomy}_{$field}", $value, $term_id, $context ); } if ( 'attribute' === $context ) { $value = esc_attr( $value ); } elseif ( 'js' === $context ) { $value = esc_js( $value ); } // Restore the type for integer fields after esc_attr(). if ( in_array( $field, $int_fields, true ) ) { $value = (int) $value; } return $value; } /** * Counts how many terms are in taxonomy. * * Default $args is 'hide_empty' which can be 'hide_empty=true' or array('hide_empty' => true). * * @since 2.3.0 * @since 5.6.0 Changed the function signature so that the `$args` array can be provided as the first parameter. * * @internal The `$deprecated` parameter is parsed for backward compatibility only. * * @param array|string $args Optional. Array or string of arguments. See WP_Term_Query::__construct() * for information on accepted arguments. Default empty array. * @param array|string $deprecated Optional. Argument array, when using the legacy function parameter format. * If present, this parameter will be interpreted as `$args`, and the first * function parameter will be parsed as a taxonomy or array of taxonomies. * Default empty. * @return string|WP_Error Numeric string containing the number of terms in that * taxonomy or WP_Error if the taxonomy does not exist. */ function wp_count_terms( $args = array(), $deprecated = '' ) { $use_legacy_args = false; // Check whether function is used with legacy signature: `$taxonomy` and `$args`. if ( $args && ( is_string( $args ) && taxonomy_exists( $args ) || is_array( $args ) && wp_is_numeric_array( $args ) ) ) { $use_legacy_args = true; } $defaults = array( 'hide_empty' => false ); if ( $use_legacy_args ) { $defaults['taxonomy'] = $args; $args = $deprecated; } $args = wp_parse_args( $args, $defaults ); // Backward compatibility. if ( isset( $args['ignore_empty'] ) ) { $args['hide_empty'] = $args['ignore_empty']; unset( $args['ignore_empty'] ); } $args['fields'] = 'count'; return get_terms( $args ); } /** * Unlinks the object from the taxonomy or taxonomies. * * Will remove all relationships between the object and any terms in * a particular taxonomy or taxonomies. Does not remove the term or * taxonomy itself. * * @since 2.3.0 * * @param int $object_id The term object ID that refers to the term. * @param string|array $taxonomies List of taxonomy names or single taxonomy name. */ function wp_delete_object_term_relationships( $object_id, $taxonomies ) { $object_id = (int) $object_id; if ( ! is_array( $taxonomies ) ) { $taxonomies = array( $taxonomies ); } foreach ( (array) $taxonomies as $taxonomy ) { $term_ids = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'ids' ) ); $term_ids = array_map( 'intval', $term_ids ); wp_remove_object_terms( $object_id, $term_ids, $taxonomy ); } } /** * Removes a term from the database. * * If the term is a parent of other terms, then the children will be updated to * that term's parent. * * Metadata associated with the term will be deleted. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term Term ID. * @param string $taxonomy Taxonomy name. * @param array|string $args { * Optional. Array of arguments to override the default term ID. Default empty array. * * @type int $default The term ID to make the default term. This will only override * the terms found if there is only one term found. Any other and * the found terms are used. * @type bool $force_default Optional. Whether to force the supplied term as default to be * assigned even if the object was not going to be term-less. * Default false. * } * @return bool|int|WP_Error True on success, false if term does not exist. Zero on attempted * deletion of default Category. WP_Error if the taxonomy does not exist. */ function wp_delete_term( $term, $taxonomy, $args = array() ) { global $wpdb; $term = (int) $term; $ids = term_exists( $term, $taxonomy ); if ( ! $ids ) { return false; } if ( is_wp_error( $ids ) ) { return $ids; } $tt_id = $ids['term_taxonomy_id']; $defaults = array(); if ( 'category' === $taxonomy ) { $defaults['default'] = (int) get_option( 'default_category' ); if ( $defaults['default'] === $term ) { return 0; // Don't delete the default category. } } // Don't delete the default custom taxonomy term. $taxonomy_object = get_taxonomy( $taxonomy ); if ( ! empty( $taxonomy_object->default_term ) ) { $defaults['default'] = (int) get_option( 'default_term_' . $taxonomy ); if ( $defaults['default'] === $term ) { return 0; } } $args = wp_parse_args( $args, $defaults ); if ( isset( $args['default'] ) ) { $default = (int) $args['default']; if ( ! term_exists( $default, $taxonomy ) ) { unset( $default ); } } if ( isset( $args['force_default'] ) ) { $force_default = $args['force_default']; } /** * Fires when deleting a term, before any modifications are made to posts or terms. * * @since 4.1.0 * * @param int $term Term ID. * @param string $taxonomy Taxonomy name. */ do_action( 'pre_delete_term', $term, $taxonomy ); // Update children to point to new parent. if ( is_taxonomy_hierarchical( $taxonomy ) ) { $term_obj = get_term( $term, $taxonomy ); if ( is_wp_error( $term_obj ) ) { return $term_obj; } $parent = $term_obj->parent; $edit_ids = $wpdb->get_results( "SELECT term_id, term_taxonomy_id FROM $wpdb->term_taxonomy WHERE `parent` = " . (int) $term_obj->term_id ); $edit_tt_ids = wp_list_pluck( $edit_ids, 'term_taxonomy_id' ); /** * Fires immediately before a term to delete's children are reassigned a parent. * * @since 2.9.0 * * @param array $edit_tt_ids An array of term taxonomy IDs for the given term. */ do_action( 'edit_term_taxonomies', $edit_tt_ids ); $wpdb->update( $wpdb->term_taxonomy, compact( 'parent' ), array( 'parent' => $term_obj->term_id ) + compact( 'taxonomy' ) ); // Clean the cache for all child terms. $edit_term_ids = wp_list_pluck( $edit_ids, 'term_id' ); clean_term_cache( $edit_term_ids, $taxonomy ); /** * Fires immediately after a term to delete's children are reassigned a parent. * * @since 2.9.0 * * @param array $edit_tt_ids An array of term taxonomy IDs for the given term. */ do_action( 'edited_term_taxonomies', $edit_tt_ids ); } // Get the term before deleting it or its term relationships so we can pass to actions below. $deleted_term = get_term( $term, $taxonomy ); $object_ids = (array) $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $tt_id ) ); foreach ( $object_ids as $object_id ) { if ( ! isset( $default ) ) { wp_remove_object_terms( $object_id, $term, $taxonomy ); continue; } $terms = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'ids', 'orderby' => 'none', ) ); if ( 1 === count( $terms ) && isset( $default ) ) { $terms = array( $default ); } else { $terms = array_diff( $terms, array( $term ) ); if ( isset( $default ) && isset( $force_default ) && $force_default ) { $terms = array_merge( $terms, array( $default ) ); } } $terms = array_map( 'intval', $terms ); wp_set_object_terms( $object_id, $terms, $taxonomy ); } // Clean the relationship caches for all object types using this term. $tax_object = get_taxonomy( $taxonomy ); foreach ( $tax_object->object_type as $object_type ) { clean_object_term_cache( $object_ids, $object_type ); } $term_meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->termmeta WHERE term_id = %d ", $term ) ); foreach ( $term_meta_ids as $mid ) { delete_metadata_by_mid( 'term', $mid ); } /** * Fires immediately before a term taxonomy ID is deleted. * * @since 2.9.0 * * @param int $tt_id Term taxonomy ID. */ do_action( 'delete_term_taxonomy', $tt_id ); $wpdb->delete( $wpdb->term_taxonomy, array( 'term_taxonomy_id' => $tt_id ) ); /** * Fires immediately after a term taxonomy ID is deleted. * * @since 2.9.0 * * @param int $tt_id Term taxonomy ID. */ do_action( 'deleted_term_taxonomy', $tt_id ); // Delete the term if no taxonomies use it. if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy WHERE term_id = %d", $term ) ) ) { $wpdb->delete( $wpdb->terms, array( 'term_id' => $term ) ); } clean_term_cache( $term, $taxonomy ); /** * Fires after a term is deleted from the database and the cache is cleaned. * * The {@see 'delete_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.5.0 * @since 4.5.0 Introduced the `$object_ids` argument. * * @param int $term Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param WP_Term $deleted_term Copy of the already-deleted term. * @param array $object_ids List of term object IDs. */ do_action( 'delete_term', $term, $tt_id, $taxonomy, $deleted_term, $object_ids ); /** * Fires after a term in a specific taxonomy is deleted. * * The dynamic portion of the hook name, `$taxonomy`, refers to the specific * taxonomy the term belonged to. * * Possible hook names include: * * - `delete_category` * - `delete_post_tag` * * @since 2.3.0 * @since 4.5.0 Introduced the `$object_ids` argument. * * @param int $term Term ID. * @param int $tt_id Term taxonomy ID. * @param WP_Term $deleted_term Copy of the already-deleted term. * @param array $object_ids List of term object IDs. */ do_action( "delete_{$taxonomy}", $term, $tt_id, $deleted_term, $object_ids ); return true; } /** * Deletes one existing category. * * @since 2.0.0 * * @param int $cat_id Category term ID. * @return bool|int|WP_Error Returns true if completes delete action; false if term doesn't exist; * Zero on attempted deletion of default Category; WP_Error object is * also a possibility. */ function wp_delete_category( $cat_id ) { return wp_delete_term( $cat_id, 'category' ); } /** * Retrieves the terms associated with the given object(s), in the supplied taxonomies. * * @since 2.3.0 * @since 4.2.0 Added support for 'taxonomy', 'parent', and 'term_taxonomy_id' values of `$orderby`. * Introduced `$parent` argument. * @since 4.4.0 Introduced `$meta_query` and `$update_term_meta_cache` arguments. When `$fields` is 'all' or * 'all_with_object_id', an array of `WP_Term` objects will be returned. * @since 4.7.0 Refactored to use WP_Term_Query, and to support any WP_Term_Query arguments. * @since 6.3.0 Passing `update_term_meta_cache` argument value false by default resulting in get_terms() to not * prime the term meta cache. * * @param int|int[] $object_ids The ID(s) of the object(s) to retrieve. * @param string|string[] $taxonomies The taxonomy names to retrieve terms from. * @param array|string $args See WP_Term_Query::__construct() for supported arguments. * @return WP_Term[]|int[]|string[]|string|WP_Error Array of terms, a count thereof as a numeric string, * or WP_Error if any of the taxonomies do not exist. * See WP_Term_Query::get_terms() for more information. */ function wp_get_object_terms( $object_ids, $taxonomies, $args = array() ) { if ( empty( $object_ids ) || empty( $taxonomies ) ) { return array(); } if ( ! is_array( $taxonomies ) ) { $taxonomies = array( $taxonomies ); } foreach ( $taxonomies as $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } } if ( ! is_array( $object_ids ) ) { $object_ids = array( $object_ids ); } $object_ids = array_map( 'intval', $object_ids ); $defaults = array( 'update_term_meta_cache' => false, ); $args = wp_parse_args( $args, $defaults ); /** * Filters arguments for retrieving object terms. * * @since 4.9.0 * * @param array $args An array of arguments for retrieving terms for the given object(s). * See {@see wp_get_object_terms()} for details. * @param int[] $object_ids Array of object IDs. * @param string[] $taxonomies Array of taxonomy names to retrieve terms from. */ $args = apply_filters( 'wp_get_object_terms_args', $args, $object_ids, $taxonomies ); /* * When one or more queried taxonomies is registered with an 'args' array, * those params override the `$args` passed to this function. */ $terms = array(); if ( count( $taxonomies ) > 1 ) { foreach ( $taxonomies as $index => $taxonomy ) { $t = get_taxonomy( $taxonomy ); if ( isset( $t->args ) && is_array( $t->args ) && array_merge( $args, $t->args ) != $args ) { unset( $taxonomies[ $index ] ); $terms = array_merge( $terms, wp_get_object_terms( $object_ids, $taxonomy, array_merge( $args, $t->args ) ) ); } } } else { $t = get_taxonomy( $taxonomies[0] ); if ( isset( $t->args ) && is_array( $t->args ) ) { $args = array_merge( $args, $t->args ); } } $args['taxonomy'] = $taxonomies; $args['object_ids'] = $object_ids; // Taxonomies registered without an 'args' param are handled here. if ( ! empty( $taxonomies ) ) { $terms_from_remaining_taxonomies = get_terms( $args ); // Array keys should be preserved for values of $fields that use term_id for keys. if ( ! empty( $args['fields'] ) && str_starts_with( $args['fields'], 'id=>' ) ) { $terms = $terms + $terms_from_remaining_taxonomies; } else { $terms = array_merge( $terms, $terms_from_remaining_taxonomies ); } } /** * Filters the terms for a given object or objects. * * @since 4.2.0 * * @param WP_Term[]|int[]|string[]|string $terms Array of terms or a count thereof as a numeric string. * @param int[] $object_ids Array of object IDs for which terms were retrieved. * @param string[] $taxonomies Array of taxonomy names from which terms were retrieved. * @param array $args Array of arguments for retrieving terms for the given * object(s). See wp_get_object_terms() for details. */ $terms = apply_filters( 'get_object_terms', $terms, $object_ids, $taxonomies, $args ); $object_ids = implode( ',', $object_ids ); $taxonomies = "'" . implode( "', '", array_map( 'esc_sql', $taxonomies ) ) . "'"; /** * Filters the terms for a given object or objects. * * The `$taxonomies` parameter passed to this filter is formatted as a SQL fragment. The * {@see 'get_object_terms'} filter is recommended as an alternative. * * @since 2.8.0 * * @param WP_Term[]|int[]|string[]|string $terms Array of terms or a count thereof as a numeric string. * @param string $object_ids Comma separated list of object IDs for which terms were retrieved. * @param string $taxonomies SQL fragment of taxonomy names from which terms were retrieved. * @param array $args Array of arguments for retrieving terms for the given * object(s). See wp_get_object_terms() for details. */ return apply_filters( 'wp_get_object_terms', $terms, $object_ids, $taxonomies, $args ); } /** * Adds a new term to the database. * * A non-existent term is inserted in the following sequence: * 1. The term is added to the term table, then related to the taxonomy. * 2. If everything is correct, several actions are fired. * 3. The 'term_id_filter' is evaluated. * 4. The term cache is cleaned. * 5. Several more actions are fired. * 6. An array is returned containing the `term_id` and `term_taxonomy_id`. * * If the 'slug' argument is not empty, then it is checked to see if the term * is invalid. If it is not a valid, existing term, it is added and the term_id * is given. * * If the taxonomy is hierarchical, and the 'parent' argument is not empty, * the term is inserted and the term_id will be given. * * Error handling: * If `$taxonomy` does not exist or `$term` is empty, * a WP_Error object will be returned. * * If the term already exists on the same hierarchical level, * or the term slug and name are not unique, a WP_Error object will be returned. * * @global wpdb $wpdb WordPress database abstraction object. * * @since 2.3.0 * * @param string $term The term name to add. * @param string $taxonomy The taxonomy to which to add the term. * @param array|string $args { * Optional. Array or query string of arguments for inserting a term. * * @type string $alias_of Slug of the term to make this term an alias of. * Default empty string. Accepts a term slug. * @type string $description The term description. Default empty string. * @type int $parent The id of the parent term. Default 0. * @type string $slug The term slug to use. Default empty string. * } * @return array|WP_Error { * An array of the new term data, WP_Error otherwise. * * @type int $term_id The new term ID. * @type int|string $term_taxonomy_id The new term taxonomy ID. Can be a numeric string. * } */ function wp_insert_term( $term, $taxonomy, $args = array() ) { global $wpdb; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } /** * Filters a term before it is sanitized and inserted into the database. * * @since 3.0.0 * @since 6.1.0 The `$args` parameter was added. * * @param string|WP_Error $term The term name to add, or a WP_Error object if there's an error. * @param string $taxonomy Taxonomy slug. * @param array|string $args Array or query string of arguments passed to wp_insert_term(). */ $term = apply_filters( 'pre_insert_term', $term, $taxonomy, $args ); if ( is_wp_error( $term ) ) { return $term; } if ( is_int( $term ) && 0 === $term ) { return new WP_Error( 'invalid_term_id', __( 'Invalid term ID.' ) ); } if ( '' === trim( $term ) ) { return new WP_Error( 'empty_term_name', __( 'A name is required for this term.' ) ); } $defaults = array( 'alias_of' => '', 'description' => '', 'parent' => 0, 'slug' => '', ); $args = wp_parse_args( $args, $defaults ); if ( (int) $args['parent'] > 0 && ! term_exists( (int) $args['parent'] ) ) { return new WP_Error( 'missing_parent', __( 'Parent term does not exist.' ) ); } $args['name'] = $term; $args['taxonomy'] = $taxonomy; // Coerce null description to strings, to avoid database errors. $args['description'] = (string) $args['description']; $args = sanitize_term( $args, $taxonomy, 'db' ); // expected_slashed ($name) $name = wp_unslash( $args['name'] ); $description = wp_unslash( $args['description'] ); $parent = (int) $args['parent']; $slug_provided = ! empty( $args['slug'] ); if ( ! $slug_provided ) { $slug = sanitize_title( $name ); } else { $slug = $args['slug']; } $term_group = 0; if ( $args['alias_of'] ) { $alias = get_term_by( 'slug', $args['alias_of'], $taxonomy ); if ( ! empty( $alias->term_group ) ) { // The alias we want is already in a group, so let's use that one. $term_group = $alias->term_group; } elseif ( ! empty( $alias->term_id ) ) { /* * The alias is not in a group, so we create a new one * and add the alias to it. */ $term_group = $wpdb->get_var( "SELECT MAX(term_group) FROM $wpdb->terms" ) + 1; wp_update_term( $alias->term_id, $taxonomy, array( 'term_group' => $term_group, ) ); } } /* * Prevent the creation of terms with duplicate names at the same level of a taxonomy hierarchy, * unless a unique slug has been explicitly provided. */ $name_matches = get_terms( array( 'taxonomy' => $taxonomy, 'name' => $name, 'hide_empty' => false, 'parent' => $args['parent'], 'update_term_meta_cache' => false, ) ); /* * The `name` match in `get_terms()` doesn't differentiate accented characters, * so we do a stricter comparison here. */ $name_match = null; if ( $name_matches ) { foreach ( $name_matches as $_match ) { if ( strtolower( $name ) === strtolower( $_match->name ) ) { $name_match = $_match; break; } } } if ( $name_match ) { $slug_match = get_term_by( 'slug', $slug, $taxonomy ); if ( ! $slug_provided || $name_match->slug === $slug || $slug_match ) { if ( is_taxonomy_hierarchical( $taxonomy ) ) { $siblings = get_terms( array( 'taxonomy' => $taxonomy, 'get' => 'all', 'parent' => $parent, 'update_term_meta_cache' => false, ) ); $existing_term = null; $sibling_names = wp_list_pluck( $siblings, 'name' ); $sibling_slugs = wp_list_pluck( $siblings, 'slug' ); if ( ( ! $slug_provided || $name_match->slug === $slug ) && in_array( $name, $sibling_names, true ) ) { $existing_term = $name_match; } elseif ( $slug_match && in_array( $slug, $sibling_slugs, true ) ) { $existing_term = $slug_match; } if ( $existing_term ) { return new WP_Error( 'term_exists', __( 'A term with the name provided already exists with this parent.' ), $existing_term->term_id ); } } else { return new WP_Error( 'term_exists', __( 'A term with the name provided already exists in this taxonomy.' ), $name_match->term_id ); } } } $slug = wp_unique_term_slug( $slug, (object) $args ); $data = compact( 'name', 'slug', 'term_group' ); /** * Filters term data before it is inserted into the database. * * @since 4.7.0 * * @param array $data Term data to be inserted. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_insert_term(). */ $data = apply_filters( 'wp_insert_term_data', $data, $taxonomy, $args ); if ( false === $wpdb->insert( $wpdb->terms, $data ) ) { return new WP_Error( 'db_insert_error', __( 'Could not insert term into the database.' ), $wpdb->last_error ); } $term_id = (int) $wpdb->insert_id; // Seems unreachable. However, is used in the case that a term name is provided, which sanitizes to an empty string. if ( empty( $slug ) ) { $slug = sanitize_title( $slug, $term_id ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edit_terms', $term_id, $taxonomy ); $wpdb->update( $wpdb->terms, compact( 'slug' ), compact( 'term_id' ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edited_terms', $term_id, $taxonomy ); } $tt_id = $wpdb->get_var( $wpdb->prepare( "SELECT tt.term_taxonomy_id FROM $wpdb->term_taxonomy AS tt INNER JOIN $wpdb->terms AS t ON tt.term_id = t.term_id WHERE tt.taxonomy = %s AND t.term_id = %d", $taxonomy, $term_id ) ); if ( ! empty( $tt_id ) ) { return array( 'term_id' => $term_id, 'term_taxonomy_id' => $tt_id, ); } if ( false === $wpdb->insert( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent' ) + array( 'count' => 0 ) ) ) { return new WP_Error( 'db_insert_error', __( 'Could not insert term taxonomy into the database.' ), $wpdb->last_error ); } $tt_id = (int) $wpdb->insert_id; /* * Sanity check: if we just created a term with the same parent + taxonomy + slug but a higher term_id than * an existing term, then we have unwittingly created a duplicate term. Delete the dupe, and use the term_id * and term_taxonomy_id of the older term instead. Then return out of the function so that the "create" hooks * are not fired. */ $duplicate_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.term_id, t.slug, tt.term_taxonomy_id, tt.taxonomy FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON ( tt.term_id = t.term_id ) WHERE t.slug = %s AND tt.parent = %d AND tt.taxonomy = %s AND t.term_id < %d AND tt.term_taxonomy_id != %d", $slug, $parent, $taxonomy, $term_id, $tt_id ) ); /** * Filters the duplicate term check that takes place during term creation. * * Term parent + taxonomy + slug combinations are meant to be unique, and wp_insert_term() * performs a last-minute confirmation of this uniqueness before allowing a new term * to be created. Plugins with different uniqueness requirements may use this filter * to bypass or modify the duplicate-term check. * * @since 5.1.0 * * @param object $duplicate_term Duplicate term row from terms table, if found. * @param string $term Term being inserted. * @param string $taxonomy Taxonomy name. * @param array $args Arguments passed to wp_insert_term(). * @param int $tt_id term_taxonomy_id for the newly created term. */ $duplicate_term = apply_filters( 'wp_insert_term_duplicate_term_check', $duplicate_term, $term, $taxonomy, $args, $tt_id ); if ( $duplicate_term ) { $wpdb->delete( $wpdb->terms, array( 'term_id' => $term_id ) ); $wpdb->delete( $wpdb->term_taxonomy, array( 'term_taxonomy_id' => $tt_id ) ); $term_id = (int) $duplicate_term->term_id; $tt_id = (int) $duplicate_term->term_taxonomy_id; clean_term_cache( $term_id, $taxonomy ); return array( 'term_id' => $term_id, 'term_taxonomy_id' => $tt_id, ); } /** * Fires immediately after a new term is created, before the term cache is cleaned. * * The {@see 'create_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_insert_term(). */ do_action( 'create_term', $term_id, $tt_id, $taxonomy, $args ); /** * Fires after a new term is created for a specific taxonomy. * * The dynamic portion of the hook name, `$taxonomy`, refers * to the slug of the taxonomy the term was created for. * * Possible hook names include: * * - `create_category` * - `create_post_tag` * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_insert_term(). */ do_action( "create_{$taxonomy}", $term_id, $tt_id, $args ); /** * Filters the term ID after a new term is created. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_insert_term(). */ $term_id = apply_filters( 'term_id_filter', $term_id, $tt_id, $args ); clean_term_cache( $term_id, $taxonomy ); /** * Fires after a new term is created, and after the term cache has been cleaned. * * The {@see 'created_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_insert_term(). */ do_action( 'created_term', $term_id, $tt_id, $taxonomy, $args ); /** * Fires after a new term in a specific taxonomy is created, and after the term * cache has been cleaned. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * Possible hook names include: * * - `created_category` * - `created_post_tag` * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_insert_term(). */ do_action( "created_{$taxonomy}", $term_id, $tt_id, $args ); /** * Fires after a term has been saved, and the term cache has been cleared. * * The {@see 'saved_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 5.5.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param bool $update Whether this is an existing term being updated. * @param array $args Arguments passed to wp_insert_term(). */ do_action( 'saved_term', $term_id, $tt_id, $taxonomy, false, $args ); /** * Fires after a term in a specific taxonomy has been saved, and the term * cache has been cleared. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * Possible hook names include: * * - `saved_category` * - `saved_post_tag` * * @since 5.5.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param bool $update Whether this is an existing term being updated. * @param array $args Arguments passed to wp_insert_term(). */ do_action( "saved_{$taxonomy}", $term_id, $tt_id, false, $args ); return array( 'term_id' => $term_id, 'term_taxonomy_id' => $tt_id, ); } /** * Creates term and taxonomy relationships. * * Relates an object (post, link, etc.) to a term and taxonomy type. Creates the * term and taxonomy relationship if it doesn't already exist. Creates a term if * it doesn't exist (using the slug). * * A relationship means that the term is grouped in or belongs to the taxonomy. * A term has no meaning until it is given context by defining which taxonomy it * exists under. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $object_id The object to relate to. * @param string|int|array $terms A single term slug, single term ID, or array of either term slugs or IDs. * Will replace all existing related terms in this taxonomy. Passing an * empty array will remove all related terms. * @param string $taxonomy The context in which to relate the term to the object. * @param bool $append Optional. If false will delete difference of terms. Default false. * @return array|WP_Error Term taxonomy IDs of the affected terms or WP_Error on failure. */ function wp_set_object_terms( $object_id, $terms, $taxonomy, $append = false ) { global $wpdb; $object_id = (int) $object_id; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } if ( empty( $terms ) ) { $terms = array(); } elseif ( ! is_array( $terms ) ) { $terms = array( $terms ); } if ( ! $append ) { $old_tt_ids = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'tt_ids', 'orderby' => 'none', 'update_term_meta_cache' => false, ) ); } else { $old_tt_ids = array(); } $tt_ids = array(); $term_ids = array(); $new_tt_ids = array(); foreach ( (array) $terms as $term ) { if ( '' === trim( $term ) ) { continue; } $term_info = term_exists( $term, $taxonomy ); if ( ! $term_info ) { // Skip if a non-existent term ID is passed. if ( is_int( $term ) ) { continue; } $term_info = wp_insert_term( $term, $taxonomy ); } if ( is_wp_error( $term_info ) ) { return $term_info; } $term_ids[] = $term_info['term_id']; $tt_id = $term_info['term_taxonomy_id']; $tt_ids[] = $tt_id; if ( $wpdb->get_var( $wpdb->prepare( "SELECT term_taxonomy_id FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id = %d", $object_id, $tt_id ) ) ) { continue; } /** * Fires immediately before an object-term relationship is added. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action( 'add_term_relationship', $object_id, $tt_id, $taxonomy ); $wpdb->insert( $wpdb->term_relationships, array( 'object_id' => $object_id, 'term_taxonomy_id' => $tt_id, ) ); /** * Fires immediately after an object-term relationship is added. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action( 'added_term_relationship', $object_id, $tt_id, $taxonomy ); $new_tt_ids[] = $tt_id; } if ( $new_tt_ids ) { wp_update_term_count( $new_tt_ids, $taxonomy ); } if ( ! $append ) { $delete_tt_ids = array_diff( $old_tt_ids, $tt_ids ); if ( $delete_tt_ids ) { $in_delete_tt_ids = "'" . implode( "', '", $delete_tt_ids ) . "'"; $delete_term_ids = $wpdb->get_col( $wpdb->prepare( "SELECT tt.term_id FROM $wpdb->term_taxonomy AS tt WHERE tt.taxonomy = %s AND tt.term_taxonomy_id IN ($in_delete_tt_ids)", $taxonomy ) ); $delete_term_ids = array_map( 'intval', $delete_term_ids ); $remove = wp_remove_object_terms( $object_id, $delete_term_ids, $taxonomy ); if ( is_wp_error( $remove ) ) { return $remove; } } } $t = get_taxonomy( $taxonomy ); if ( ! $append && isset( $t->sort ) && $t->sort ) { $values = array(); $term_order = 0; $final_tt_ids = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'tt_ids', 'update_term_meta_cache' => false, ) ); foreach ( $tt_ids as $tt_id ) { if ( in_array( (int) $tt_id, $final_tt_ids, true ) ) { $values[] = $wpdb->prepare( '(%d, %d, %d)', $object_id, $tt_id, ++$term_order ); } } if ( $values ) { if ( false === $wpdb->query( "INSERT INTO $wpdb->term_relationships (object_id, term_taxonomy_id, term_order) VALUES " . implode( ',', $values ) . ' ON DUPLICATE KEY UPDATE term_order = VALUES(term_order)' ) ) { return new WP_Error( 'db_insert_error', __( 'Could not insert term relationship into the database.' ), $wpdb->last_error ); } } } wp_cache_delete( $object_id, $taxonomy . '_relationships' ); wp_cache_set_terms_last_changed(); /** * Fires after an object's terms have been set. * * @since 2.8.0 * * @param int $object_id Object ID. * @param array $terms An array of object term IDs or slugs. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. * @param bool $append Whether to append new terms to the old terms. * @param array $old_tt_ids Old array of term taxonomy IDs. */ do_action( 'set_object_terms', $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ); return $tt_ids; } /** * Adds term(s) associated with a given object. * * @since 3.6.0 * * @param int $object_id The ID of the object to which the terms will be added. * @param string|int|array $terms The slug(s) or ID(s) of the term(s) to add. * @param array|string $taxonomy Taxonomy name. * @return array|WP_Error Term taxonomy IDs of the affected terms. */ function wp_add_object_terms( $object_id, $terms, $taxonomy ) { return wp_set_object_terms( $object_id, $terms, $taxonomy, true ); } /** * Removes term(s) associated with a given object. * * @since 3.6.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $object_id The ID of the object from which the terms will be removed. * @param string|int|array $terms The slug(s) or ID(s) of the term(s) to remove. * @param string $taxonomy Taxonomy name. * @return bool|WP_Error True on success, false or WP_Error on failure. */ function wp_remove_object_terms( $object_id, $terms, $taxonomy ) { global $wpdb; $object_id = (int) $object_id; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } if ( ! is_array( $terms ) ) { $terms = array( $terms ); } $tt_ids = array(); foreach ( (array) $terms as $term ) { if ( '' === trim( $term ) ) { continue; } $term_info = term_exists( $term, $taxonomy ); if ( ! $term_info ) { // Skip if a non-existent term ID is passed. if ( is_int( $term ) ) { continue; } } if ( is_wp_error( $term_info ) ) { return $term_info; } $tt_ids[] = $term_info['term_taxonomy_id']; } if ( $tt_ids ) { $in_tt_ids = "'" . implode( "', '", $tt_ids ) . "'"; /** * Fires immediately before an object-term relationship is deleted. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. */ do_action( 'delete_term_relationships', $object_id, $tt_ids, $taxonomy ); $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id IN ($in_tt_ids)", $object_id ) ); wp_cache_delete( $object_id, $taxonomy . '_relationships' ); wp_cache_set_terms_last_changed(); /** * Fires immediately after an object-term relationship is deleted. * * @since 2.9.0 * @since 4.7.0 Added the `$taxonomy` parameter. * * @param int $object_id Object ID. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. */ do_action( 'deleted_term_relationships', $object_id, $tt_ids, $taxonomy ); wp_update_term_count( $tt_ids, $taxonomy ); return (bool) $deleted; } return false; } /** * Makes term slug unique, if it isn't already. * * The `$slug` has to be unique global to every taxonomy, meaning that one * taxonomy term can't have a matching slug with another taxonomy term. Each * slug has to be globally unique for every taxonomy. * * The way this works is that if the taxonomy that the term belongs to is * hierarchical and has a parent, it will append that parent to the $slug. * * If that still doesn't return a unique slug, then it tries to append a number * until it finds a number that is truly unique. * * The only purpose for `$term` is for appending a parent, if one exists. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $slug The string that will be tried for a unique slug. * @param object $term The term object that the `$slug` will belong to. * @return string Will return a true unique slug. */ function wp_unique_term_slug( $slug, $term ) { global $wpdb; $needs_suffix = true; $original_slug = $slug; // As of 4.1, duplicate slugs are allowed as long as they're in different taxonomies. if ( ! term_exists( $slug ) || get_option( 'db_version' ) >= 30133 && ! get_term_by( 'slug', $slug, $term->taxonomy ) ) { $needs_suffix = false; } /* * If the taxonomy supports hierarchy and the term has a parent, make the slug unique * by incorporating parent slugs. */ $parent_suffix = ''; if ( $needs_suffix && is_taxonomy_hierarchical( $term->taxonomy ) && ! empty( $term->parent ) ) { $the_parent = $term->parent; while ( ! empty( $the_parent ) ) { $parent_term = get_term( $the_parent, $term->taxonomy ); if ( is_wp_error( $parent_term ) || empty( $parent_term ) ) { break; } $parent_suffix .= '-' . $parent_term->slug; if ( ! term_exists( $slug . $parent_suffix ) ) { break; } if ( empty( $parent_term->parent ) ) { break; } $the_parent = $parent_term->parent; } } // If we didn't get a unique slug, try appending a number to make it unique. /** * Filters whether the proposed unique term slug is bad. * * @since 4.3.0 * * @param bool $needs_suffix Whether the slug needs to be made unique with a suffix. * @param string $slug The slug. * @param object $term Term object. */ if ( apply_filters( 'wp_unique_term_slug_is_bad_slug', $needs_suffix, $slug, $term ) ) { if ( $parent_suffix ) { $slug .= $parent_suffix; } if ( ! empty( $term->term_id ) ) { $query = $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s AND term_id != %d", $slug, $term->term_id ); } else { $query = $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s", $slug ); } if ( $wpdb->get_var( $query ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $num = 2; do { $alt_slug = $slug . "-$num"; ++$num; $slug_check = $wpdb->get_var( $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s", $alt_slug ) ); } while ( $slug_check ); $slug = $alt_slug; } } /** * Filters the unique term slug. * * @since 4.3.0 * * @param string $slug Unique term slug. * @param object $term Term object. * @param string $original_slug Slug originally passed to the function for testing. */ return apply_filters( 'wp_unique_term_slug', $slug, $term, $original_slug ); } /** * Updates term based on arguments provided. * * The `$args` will indiscriminately override all values with the same field name. * Care must be taken to not override important information need to update or * update will fail (or perhaps create a new term, neither would be acceptable). * * Defaults will set 'alias_of', 'description', 'parent', and 'slug' if not * defined in `$args` already. * * 'alias_of' will create a term group, if it doesn't already exist, and * update it for the `$term`. * * If the 'slug' argument in `$args` is missing, then the 'name' will be used. * If you set 'slug' and it isn't unique, then a WP_Error is returned. * If you don't pass any slug, then a unique one will be created. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id The ID of the term. * @param string $taxonomy The taxonomy of the term. * @param array $args { * Optional. Array of arguments for updating a term. * * @type string $alias_of Slug of the term to make this term an alias of. * Default empty string. Accepts a term slug. * @type string $description The term description. Default empty string. * @type int $parent The id of the parent term. Default 0. * @type string $slug The term slug to use. Default empty string. * } * @return array|WP_Error An array containing the `term_id` and `term_taxonomy_id`, * WP_Error otherwise. */ function wp_update_term( $term_id, $taxonomy, $args = array() ) { global $wpdb; if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); } $term_id = (int) $term_id; // First, get all of the original args. $term = get_term( $term_id, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } if ( ! $term ) { return new WP_Error( 'invalid_term', __( 'Empty Term.' ) ); } $term = (array) $term->data; // Escape data pulled from DB. $term = wp_slash( $term ); // Merge old and new args with new args overwriting old ones. $args = array_merge( $term, $args ); $defaults = array( 'alias_of' => '', 'description' => '', 'parent' => 0, 'slug' => '', ); $args = wp_parse_args( $args, $defaults ); $args = sanitize_term( $args, $taxonomy, 'db' ); $parsed_args = $args; // expected_slashed ($name) $name = wp_unslash( $args['name'] ); $description = wp_unslash( $args['description'] ); $parsed_args['name'] = $name; $parsed_args['description'] = $description; if ( '' === trim( $name ) ) { return new WP_Error( 'empty_term_name', __( 'A name is required for this term.' ) ); } if ( (int) $parsed_args['parent'] > 0 && ! term_exists( (int) $parsed_args['parent'] ) ) { return new WP_Error( 'missing_parent', __( 'Parent term does not exist.' ) ); } $empty_slug = false; if ( empty( $args['slug'] ) ) { $empty_slug = true; $slug = sanitize_title( $name ); } else { $slug = $args['slug']; } $parsed_args['slug'] = $slug; $term_group = isset( $parsed_args['term_group'] ) ? $parsed_args['term_group'] : 0; if ( $args['alias_of'] ) { $alias = get_term_by( 'slug', $args['alias_of'], $taxonomy ); if ( ! empty( $alias->term_group ) ) { // The alias we want is already in a group, so let's use that one. $term_group = $alias->term_group; } elseif ( ! empty( $alias->term_id ) ) { /* * The alias is not in a group, so we create a new one * and add the alias to it. */ $term_group = $wpdb->get_var( "SELECT MAX(term_group) FROM $wpdb->terms" ) + 1; wp_update_term( $alias->term_id, $taxonomy, array( 'term_group' => $term_group, ) ); } $parsed_args['term_group'] = $term_group; } /** * Filters the term parent. * * Hook to this filter to see if it will cause a hierarchy loop. * * @since 3.1.0 * * @param int $parent_term ID of the parent term. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $parsed_args An array of potentially altered update arguments for the given term. * @param array $args Arguments passed to wp_update_term(). */ $parent = (int) apply_filters( 'wp_update_term_parent', $args['parent'], $term_id, $taxonomy, $parsed_args, $args ); // Check for duplicate slug. $duplicate = get_term_by( 'slug', $slug, $taxonomy ); if ( $duplicate && $duplicate->term_id !== $term_id ) { /* * If an empty slug was passed or the parent changed, reset the slug to something unique. * Otherwise, bail. */ if ( $empty_slug || ( $parent !== (int) $term['parent'] ) ) { $slug = wp_unique_term_slug( $slug, (object) $args ); } else { /* translators: %s: Taxonomy term slug. */ return new WP_Error( 'duplicate_term_slug', sprintf( __( 'The slug “%s” is already in use by another term.' ), $slug ) ); } } $tt_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT tt.term_taxonomy_id FROM $wpdb->term_taxonomy AS tt INNER JOIN $wpdb->terms AS t ON tt.term_id = t.term_id WHERE tt.taxonomy = %s AND t.term_id = %d", $taxonomy, $term_id ) ); // Check whether this is a shared term that needs splitting. $_term_id = _split_shared_term( $term_id, $tt_id ); if ( ! is_wp_error( $_term_id ) ) { $term_id = $_term_id; } /** * Fires immediately before the given terms are edited. * * @since 2.9.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edit_terms', $term_id, $taxonomy, $args ); $data = compact( 'name', 'slug', 'term_group' ); /** * Filters term data before it is updated in the database. * * @since 4.7.0 * * @param array $data Term data to be updated. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ $data = apply_filters( 'wp_update_term_data', $data, $term_id, $taxonomy, $args ); $wpdb->update( $wpdb->terms, $data, compact( 'term_id' ) ); if ( empty( $slug ) ) { $slug = sanitize_title( $name, $term_id ); $wpdb->update( $wpdb->terms, compact( 'slug' ), compact( 'term_id' ) ); } /** * Fires immediately after a term is updated in the database, but before its * term-taxonomy relationship is updated. * * @since 2.9.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edited_terms', $term_id, $taxonomy, $args ); /** * Fires immediate before a term-taxonomy relationship is updated. * * @since 2.9.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edit_term_taxonomy', $tt_id, $taxonomy, $args ); $wpdb->update( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent' ), array( 'term_taxonomy_id' => $tt_id ) ); /** * Fires immediately after a term-taxonomy relationship is updated. * * @since 2.9.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edited_term_taxonomy', $tt_id, $taxonomy, $args ); /** * Fires after a term has been updated, but before the term cache has been cleaned. * * The {@see 'edit_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edit_term', $term_id, $tt_id, $taxonomy, $args ); /** * Fires after a term in a specific taxonomy has been updated, but before the term * cache has been cleaned. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * Possible hook names include: * * - `edit_category` * - `edit_post_tag` * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_update_term(). */ do_action( "edit_{$taxonomy}", $term_id, $tt_id, $args ); /** This filter is documented in wp-includes/taxonomy.php */ $term_id = apply_filters( 'term_id_filter', $term_id, $tt_id ); clean_term_cache( $term_id, $taxonomy ); /** * Fires after a term has been updated, and the term cache has been cleaned. * * The {@see 'edited_$taxonomy'} hook is also available for targeting a specific * taxonomy. * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @param array $args Arguments passed to wp_update_term(). */ do_action( 'edited_term', $term_id, $tt_id, $taxonomy, $args ); /** * Fires after a term for a specific taxonomy has been updated, and the term * cache has been cleaned. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * Possible hook names include: * * - `edited_category` * - `edited_post_tag` * * @since 2.3.0 * @since 6.1.0 The `$args` parameter was added. * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param array $args Arguments passed to wp_update_term(). */ do_action( "edited_{$taxonomy}", $term_id, $tt_id, $args ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'saved_term', $term_id, $tt_id, $taxonomy, true, $args ); /** This action is documented in wp-includes/taxonomy.php */ do_action( "saved_{$taxonomy}", $term_id, $tt_id, true, $args ); return array( 'term_id' => $term_id, 'term_taxonomy_id' => $tt_id, ); } /** * Enables or disables term counting. * * @since 2.5.0 * * @param bool $defer Optional. Enable if true, disable if false. * @return bool Whether term counting is enabled or disabled. */ function wp_defer_term_counting( $defer = null ) { static $_defer = false; if ( is_bool( $defer ) ) { $_defer = $defer; // Flush any deferred counts. if ( ! $defer ) { wp_update_term_count( null, null, true ); } } return $_defer; } /** * Updates the amount of terms in taxonomy. * * If there is a taxonomy callback applied, then it will be called for updating * the count. * * The default action is to count what the amount of terms have the relationship * of term ID. Once that is done, then update the database. * * @since 2.3.0 * * @param int|array $terms The term_taxonomy_id of the terms. * @param string $taxonomy The context of the term. * @param bool $do_deferred Whether to flush the deferred term counts too. Default false. * @return bool If no terms will return false, and if successful will return true. */ function wp_update_term_count( $terms, $taxonomy, $do_deferred = false ) { static $_deferred = array(); if ( $do_deferred ) { foreach ( (array) array_keys( $_deferred ) as $tax ) { wp_update_term_count_now( $_deferred[ $tax ], $tax ); unset( $_deferred[ $tax ] ); } } if ( empty( $terms ) ) { return false; } if ( ! is_array( $terms ) ) { $terms = array( $terms ); } if ( wp_defer_term_counting() ) { if ( ! isset( $_deferred[ $taxonomy ] ) ) { $_deferred[ $taxonomy ] = array(); } $_deferred[ $taxonomy ] = array_unique( array_merge( $_deferred[ $taxonomy ], $terms ) ); return true; } return wp_update_term_count_now( $terms, $taxonomy ); } /** * Performs term count update immediately. * * @since 2.5.0 * * @param array $terms The term_taxonomy_id of terms to update. * @param string $taxonomy The context of the term. * @return true Always true when complete. */ function wp_update_term_count_now( $terms, $taxonomy ) { $terms = array_map( 'intval', $terms ); $taxonomy = get_taxonomy( $taxonomy ); if ( ! empty( $taxonomy->update_count_callback ) ) { call_user_func( $taxonomy->update_count_callback, $terms, $taxonomy ); } else { $object_types = (array) $taxonomy->object_type; foreach ( $object_types as &$object_type ) { if ( str_starts_with( $object_type, 'attachment:' ) ) { list( $object_type ) = explode( ':', $object_type ); } } if ( array_filter( $object_types, 'post_type_exists' ) == $object_types ) { // Only post types are attached to this taxonomy. _update_post_term_count( $terms, $taxonomy ); } else { // Default count updater. _update_generic_term_count( $terms, $taxonomy ); } } clean_term_cache( $terms, '', false ); return true; } // // Cache. // /** * Removes the taxonomy relationship to terms from the cache. * * Will remove the entire taxonomy relationship containing term `$object_id`. The * term IDs have to exist within the taxonomy `$object_type` for the deletion to * take place. * * @since 2.3.0 * * @global bool $_wp_suspend_cache_invalidation * * @see get_object_taxonomies() for more on $object_type. * * @param int|array $object_ids Single or list of term object ID(s). * @param array|string $object_type The taxonomy object type. */ function clean_object_term_cache( $object_ids, $object_type ) { global $_wp_suspend_cache_invalidation; if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } if ( ! is_array( $object_ids ) ) { $object_ids = array( $object_ids ); } $taxonomies = get_object_taxonomies( $object_type ); foreach ( $taxonomies as $taxonomy ) { wp_cache_delete_multiple( $object_ids, "{$taxonomy}_relationships" ); } wp_cache_set_terms_last_changed(); /** * Fires after the object term cache has been cleaned. * * @since 2.5.0 * * @param array $object_ids An array of object IDs. * @param string $object_type Object type. */ do_action( 'clean_object_term_cache', $object_ids, $object_type ); } /** * Removes all of the term IDs from the cache. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * @global bool $_wp_suspend_cache_invalidation * * @param int|int[] $ids Single or array of term IDs. * @param string $taxonomy Optional. Taxonomy slug. Can be empty, in which case the taxonomies of the passed * term IDs will be used. Default empty. * @param bool $clean_taxonomy Optional. Whether to clean taxonomy wide caches (true), or just individual * term object caches (false). Default true. */ function clean_term_cache( $ids, $taxonomy = '', $clean_taxonomy = true ) { global $wpdb, $_wp_suspend_cache_invalidation; if ( ! empty( $_wp_suspend_cache_invalidation ) ) { return; } if ( ! is_array( $ids ) ) { $ids = array( $ids ); } $taxonomies = array(); // If no taxonomy, assume tt_ids. if ( empty( $taxonomy ) ) { $tt_ids = array_map( 'intval', $ids ); $tt_ids = implode( ', ', $tt_ids ); $terms = $wpdb->get_results( "SELECT term_id, taxonomy FROM $wpdb->term_taxonomy WHERE term_taxonomy_id IN ($tt_ids)" ); $ids = array(); foreach ( (array) $terms as $term ) { $taxonomies[] = $term->taxonomy; $ids[] = $term->term_id; } wp_cache_delete_multiple( $ids, 'terms' ); $taxonomies = array_unique( $taxonomies ); } else { wp_cache_delete_multiple( $ids, 'terms' ); $taxonomies = array( $taxonomy ); } foreach ( $taxonomies as $taxonomy ) { if ( $clean_taxonomy ) { clean_taxonomy_cache( $taxonomy ); } /** * Fires once after each taxonomy's term cache has been cleaned. * * @since 2.5.0 * @since 4.5.0 Added the `$clean_taxonomy` parameter. * * @param array $ids An array of term IDs. * @param string $taxonomy Taxonomy slug. * @param bool $clean_taxonomy Whether or not to clean taxonomy-wide caches */ do_action( 'clean_term_cache', $ids, $taxonomy, $clean_taxonomy ); } wp_cache_set_terms_last_changed(); } /** * Cleans the caches for a taxonomy. * * @since 4.9.0 * * @param string $taxonomy Taxonomy slug. */ function clean_taxonomy_cache( $taxonomy ) { wp_cache_delete( 'all_ids', $taxonomy ); wp_cache_delete( 'get', $taxonomy ); wp_cache_set_terms_last_changed(); // Regenerate cached hierarchy. delete_option( "{$taxonomy}_children" ); _get_term_hierarchy( $taxonomy ); /** * Fires after a taxonomy's caches have been cleaned. * * @since 4.9.0 * * @param string $taxonomy Taxonomy slug. */ do_action( 'clean_taxonomy_cache', $taxonomy ); } /** * Retrieves the cached term objects for the given object ID. * * Upstream functions (like get_the_terms() and is_object_in_term()) are * responsible for populating the object-term relationship cache. The current * function only fetches relationship data that is already in the cache. * * @since 2.3.0 * @since 4.7.0 Returns a `WP_Error` object if there's an error with * any of the matched terms. * * @param int $id Term object ID, for example a post, comment, or user ID. * @param string $taxonomy Taxonomy name. * @return bool|WP_Term[]|WP_Error Array of `WP_Term` objects, if cached. * False if cache is empty for `$taxonomy` and `$id`. * WP_Error if get_term() returns an error object for any term. */ function get_object_term_cache( $id, $taxonomy ) { $_term_ids = wp_cache_get( $id, "{$taxonomy}_relationships" ); // We leave the priming of relationship caches to upstream functions. if ( false === $_term_ids ) { return false; } // Backward compatibility for if a plugin is putting objects into the cache, rather than IDs. $term_ids = array(); foreach ( $_term_ids as $term_id ) { if ( is_numeric( $term_id ) ) { $term_ids[] = (int) $term_id; } elseif ( isset( $term_id->term_id ) ) { $term_ids[] = (int) $term_id->term_id; } } // Fill the term objects. _prime_term_caches( $term_ids ); $terms = array(); foreach ( $term_ids as $term_id ) { $term = get_term( $term_id, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } $terms[] = $term; } return $terms; } /** * Updates the cache for the given term object ID(s). * * Note: Due to performance concerns, great care should be taken to only update * term caches when necessary. Processing time can increase exponentially depending * on both the number of passed term IDs and the number of taxonomies those terms * belong to. * * Caches will only be updated for terms not already cached. * * @since 2.3.0 * * @param string|int[] $object_ids Comma-separated list or array of term object IDs. * @param string|string[] $object_type The taxonomy object type or array of the same. * @return void|false Void on success or if the `$object_ids` parameter is empty, * false if all of the terms in `$object_ids` are already cached. */ function update_object_term_cache( $object_ids, $object_type ) { if ( empty( $object_ids ) ) { return; } if ( ! is_array( $object_ids ) ) { $object_ids = explode( ',', $object_ids ); } $object_ids = array_map( 'intval', $object_ids ); $non_cached_ids = array(); $taxonomies = get_object_taxonomies( $object_type ); foreach ( $taxonomies as $taxonomy ) { $cache_values = wp_cache_get_multiple( (array) $object_ids, "{$taxonomy}_relationships" ); foreach ( $cache_values as $id => $value ) { if ( false === $value ) { $non_cached_ids[] = $id; } } } if ( empty( $non_cached_ids ) ) { return false; } $non_cached_ids = array_unique( $non_cached_ids ); $terms = wp_get_object_terms( $non_cached_ids, $taxonomies, array( 'fields' => 'all_with_object_id', 'orderby' => 'name', 'update_term_meta_cache' => false, ) ); $object_terms = array(); foreach ( (array) $terms as $term ) { $object_terms[ $term->object_id ][ $term->taxonomy ][] = $term->term_id; } foreach ( $non_cached_ids as $id ) { foreach ( $taxonomies as $taxonomy ) { if ( ! isset( $object_terms[ $id ][ $taxonomy ] ) ) { if ( ! isset( $object_terms[ $id ] ) ) { $object_terms[ $id ] = array(); } $object_terms[ $id ][ $taxonomy ] = array(); } } } $cache_values = array(); foreach ( $object_terms as $id => $value ) { foreach ( $value as $taxonomy => $terms ) { $cache_values[ $taxonomy ][ $id ] = $terms; } } foreach ( $cache_values as $taxonomy => $data ) { wp_cache_add_multiple( $data, "{$taxonomy}_relationships" ); } } /** * Updates terms in cache. * * @since 2.3.0 * * @param WP_Term[] $terms Array of term objects to change. * @param string $taxonomy Not used. */ function update_term_cache( $terms, $taxonomy = '' ) { $data = array(); foreach ( (array) $terms as $term ) { // Create a copy in case the array was passed by reference. $_term = clone $term; // Object ID should not be cached. unset( $_term->object_id ); $data[ $term->term_id ] = $_term; } wp_cache_add_multiple( $data, 'terms' ); } // // Private. // /** * Retrieves children of taxonomy as term IDs. * * @access private * @since 2.3.0 * * @param string $taxonomy Taxonomy name. * @return array Empty if $taxonomy isn't hierarchical or returns children as term IDs. */ function _get_term_hierarchy( $taxonomy ) { if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { return array(); } $children = get_option( "{$taxonomy}_children" ); if ( is_array( $children ) ) { return $children; } $children = array(); $terms = get_terms( array( 'taxonomy' => $taxonomy, 'get' => 'all', 'orderby' => 'id', 'fields' => 'id=>parent', 'update_term_meta_cache' => false, ) ); foreach ( $terms as $term_id => $parent ) { if ( $parent > 0 ) { $children[ $parent ][] = $term_id; } } update_option( "{$taxonomy}_children", $children ); return $children; } /** * Gets the subset of $terms that are descendants of $term_id. * * If `$terms` is an array of objects, then _get_term_children() returns an array of objects. * If `$terms` is an array of IDs, then _get_term_children() returns an array of IDs. * * @access private * @since 2.3.0 * * @param int $term_id The ancestor term: all returned terms should be descendants of `$term_id`. * @param array $terms The set of terms - either an array of term objects or term IDs - from which those that * are descendants of $term_id will be chosen. * @param string $taxonomy The taxonomy which determines the hierarchy of the terms. * @param array $ancestors Optional. Term ancestors that have already been identified. Passed by reference, to keep * track of found terms when recursing the hierarchy. The array of located ancestors is used * to prevent infinite recursion loops. For performance, `term_ids` are used as array keys, * with 1 as value. Default empty array. * @return array|WP_Error The subset of $terms that are descendants of $term_id. */ function _get_term_children( $term_id, $terms, $taxonomy, &$ancestors = array() ) { $empty_array = array(); if ( empty( $terms ) ) { return $empty_array; } $term_id = (int) $term_id; $term_list = array(); $has_children = _get_term_hierarchy( $taxonomy ); if ( $term_id && ! isset( $has_children[ $term_id ] ) ) { return $empty_array; } // Include the term itself in the ancestors array, so we can properly detect when a loop has occurred. if ( empty( $ancestors ) ) { $ancestors[ $term_id ] = 1; } foreach ( (array) $terms as $term ) { $use_id = false; if ( ! is_object( $term ) ) { $term = get_term( $term, $taxonomy ); if ( is_wp_error( $term ) ) { return $term; } $use_id = true; } // Don't recurse if we've already identified the term as a child - this indicates a loop. if ( isset( $ancestors[ $term->term_id ] ) ) { continue; } if ( (int) $term->parent === $term_id ) { if ( $use_id ) { $term_list[] = $term->term_id; } else { $term_list[] = $term; } if ( ! isset( $has_children[ $term->term_id ] ) ) { continue; } $ancestors[ $term->term_id ] = 1; $children = _get_term_children( $term->term_id, $terms, $taxonomy, $ancestors ); if ( $children ) { $term_list = array_merge( $term_list, $children ); } } } return $term_list; } /** * Adds count of children to parent count. * * Recalculates term counts by including items from child terms. Assumes all * relevant children are already in the $terms argument. * * @access private * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param object[]|WP_Term[] $terms List of term objects (passed by reference). * @param string $taxonomy Term context. */ function _pad_term_counts( &$terms, $taxonomy ) { global $wpdb; // This function only works for hierarchical taxonomies like post categories. if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { return; } $term_hier = _get_term_hierarchy( $taxonomy ); if ( empty( $term_hier ) ) { return; } $term_items = array(); $terms_by_id = array(); $term_ids = array(); foreach ( (array) $terms as $key => $term ) { $terms_by_id[ $term->term_id ] = & $terms[ $key ]; $term_ids[ $term->term_taxonomy_id ] = $term->term_id; } // Get the object and term IDs and stick them in a lookup table. $tax_obj = get_taxonomy( $taxonomy ); $object_types = esc_sql( $tax_obj->object_type ); $results = $wpdb->get_results( "SELECT object_id, term_taxonomy_id FROM $wpdb->term_relationships INNER JOIN $wpdb->posts ON object_id = ID WHERE term_taxonomy_id IN (" . implode( ',', array_keys( $term_ids ) ) . ") AND post_type IN ('" . implode( "', '", $object_types ) . "') AND post_status = 'publish'" ); foreach ( $results as $row ) { $id = $term_ids[ $row->term_taxonomy_id ]; $term_items[ $id ][ $row->object_id ] = isset( $term_items[ $id ][ $row->object_id ] ) ? ++$term_items[ $id ][ $row->object_id ] : 1; } // Touch every ancestor's lookup row for each post in each term. foreach ( $term_ids as $term_id ) { $child = $term_id; $ancestors = array(); while ( ! empty( $terms_by_id[ $child ] ) && $parent = $terms_by_id[ $child ]->parent ) { $ancestors[] = $child; if ( ! empty( $term_items[ $term_id ] ) ) { foreach ( $term_items[ $term_id ] as $item_id => $touches ) { $term_items[ $parent ][ $item_id ] = isset( $term_items[ $parent ][ $item_id ] ) ? ++$term_items[ $parent ][ $item_id ] : 1; } } $child = $parent; if ( in_array( $parent, $ancestors, true ) ) { break; } } } // Transfer the touched cells. foreach ( (array) $term_items as $id => $items ) { if ( isset( $terms_by_id[ $id ] ) ) { $terms_by_id[ $id ]->count = count( $items ); } } } /** * Adds any terms from the given IDs to the cache that do not already exist in cache. * * @since 4.6.0 * @since 6.1.0 This function is no longer marked as "private". * @since 6.3.0 Use wp_lazyload_term_meta() for lazy-loading of term meta. * * @global wpdb $wpdb WordPress database abstraction object. * * @param array $term_ids Array of term IDs. * @param bool $update_meta_cache Optional. Whether to update the meta cache. Default true. */ function _prime_term_caches( $term_ids, $update_meta_cache = true ) { global $wpdb; $non_cached_ids = _get_non_cached_ids( $term_ids, 'terms' ); if ( ! empty( $non_cached_ids ) ) { $fresh_terms = $wpdb->get_results( sprintf( "SELECT t.*, tt.* FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id WHERE t.term_id IN (%s)", implode( ',', array_map( 'intval', $non_cached_ids ) ) ) ); update_term_cache( $fresh_terms ); } if ( $update_meta_cache ) { wp_lazyload_term_meta( $term_ids ); } } // // Default callbacks. // /** * Updates term count based on object types of the current taxonomy. * * Private function for the default callback for post_tag and category * taxonomies. * * @access private * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int[] $terms List of term taxonomy IDs. * @param WP_Taxonomy $taxonomy Current taxonomy object of terms. */ function _update_post_term_count( $terms, $taxonomy ) { global $wpdb; $object_types = (array) $taxonomy->object_type; foreach ( $object_types as &$object_type ) { list( $object_type ) = explode( ':', $object_type ); } $object_types = array_unique( $object_types ); $check_attachments = array_search( 'attachment', $object_types, true ); if ( false !== $check_attachments ) { unset( $object_types[ $check_attachments ] ); $check_attachments = true; } if ( $object_types ) { $object_types = esc_sql( array_filter( $object_types, 'post_type_exists' ) ); } $post_statuses = array( 'publish' ); /** * Filters the post statuses for updating the term count. * * @since 5.7.0 * * @param string[] $post_statuses List of post statuses to include in the count. Default is 'publish'. * @param WP_Taxonomy $taxonomy Current taxonomy object. */ $post_statuses = esc_sql( apply_filters( 'update_post_term_count_statuses', $post_statuses, $taxonomy ) ); foreach ( (array) $terms as $term ) { $count = 0; // Attachments can be 'inherit' status, we need to base count off the parent's status if so. if ( $check_attachments ) { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration $count += (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts p1 WHERE p1.ID = $wpdb->term_relationships.object_id AND ( post_status IN ('" . implode( "', '", $post_statuses ) . "') OR ( post_status = 'inherit' AND post_parent > 0 AND ( SELECT post_status FROM $wpdb->posts WHERE ID = p1.post_parent ) IN ('" . implode( "', '", $post_statuses ) . "') ) ) AND post_type = 'attachment' AND term_taxonomy_id = %d", $term ) ); } if ( $object_types ) { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration $count += (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts WHERE $wpdb->posts.ID = $wpdb->term_relationships.object_id AND post_status IN ('" . implode( "', '", $post_statuses ) . "') AND post_type IN ('" . implode( "', '", $object_types ) . "') AND term_taxonomy_id = %d", $term ) ); } /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edit_term_taxonomy', $term, $taxonomy->name ); $wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edited_term_taxonomy', $term, $taxonomy->name ); } } /** * Updates term count based on number of objects. * * Default callback for the 'link_category' taxonomy. * * @since 3.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int[] $terms List of term taxonomy IDs. * @param WP_Taxonomy $taxonomy Current taxonomy object of terms. */ function _update_generic_term_count( $terms, $taxonomy ) { global $wpdb; foreach ( (array) $terms as $term ) { $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $term ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edit_term_taxonomy', $term, $taxonomy->name ); $wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) ); /** This action is documented in wp-includes/taxonomy.php */ do_action( 'edited_term_taxonomy', $term, $taxonomy->name ); } } /** * Creates a new term for a term_taxonomy item that currently shares its term * with another term_taxonomy. * * @ignore * @since 4.2.0 * @since 4.3.0 Introduced `$record` parameter. Also, `$term_id` and * `$term_taxonomy_id` can now accept objects. * * @global wpdb $wpdb WordPress database abstraction object. * * @param int|object $term_id ID of the shared term, or the shared term object. * @param int|object $term_taxonomy_id ID of the term_taxonomy item to receive a new term, or the term_taxonomy object * (corresponding to a row from the term_taxonomy table). * @param bool $record Whether to record data about the split term in the options table. The recording * process has the potential to be resource-intensive, so during batch operations * it can be beneficial to skip inline recording and do it just once, after the * batch is processed. Only set this to `false` if you know what you are doing. * Default: true. * @return int|WP_Error When the current term does not need to be split (or cannot be split on the current * database schema), `$term_id` is returned. When the term is successfully split, the * new term_id is returned. A WP_Error is returned for miscellaneous errors. */ function _split_shared_term( $term_id, $term_taxonomy_id, $record = true ) { global $wpdb; if ( is_object( $term_id ) ) { $shared_term = $term_id; $term_id = (int) $shared_term->term_id; } if ( is_object( $term_taxonomy_id ) ) { $term_taxonomy = $term_taxonomy_id; $term_taxonomy_id = (int) $term_taxonomy->term_taxonomy_id; } // If there are no shared term_taxonomy rows, there's nothing to do here. $shared_tt_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy tt WHERE tt.term_id = %d AND tt.term_taxonomy_id != %d", $term_id, $term_taxonomy_id ) ); if ( ! $shared_tt_count ) { return $term_id; } /* * Verify that the term_taxonomy_id passed to the function is actually associated with the term_id. * If there's a mismatch, it may mean that the term is already split. Return the actual term_id from the db. */ $check_term_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) ); if ( $check_term_id !== $term_id ) { return $check_term_id; } // Pull up data about the currently shared slug, which we'll use to populate the new one. if ( empty( $shared_term ) ) { $shared_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.* FROM $wpdb->terms t WHERE t.term_id = %d", $term_id ) ); } $new_term_data = array( 'name' => $shared_term->name, 'slug' => $shared_term->slug, 'term_group' => $shared_term->term_group, ); if ( false === $wpdb->insert( $wpdb->terms, $new_term_data ) ) { return new WP_Error( 'db_insert_error', __( 'Could not split shared term.' ), $wpdb->last_error ); } $new_term_id = (int) $wpdb->insert_id; // Update the existing term_taxonomy to point to the newly created term. $wpdb->update( $wpdb->term_taxonomy, array( 'term_id' => $new_term_id ), array( 'term_taxonomy_id' => $term_taxonomy_id ) ); // Reassign child terms to the new parent. if ( empty( $term_taxonomy ) ) { $term_taxonomy = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) ); } $children_tt_ids = $wpdb->get_col( $wpdb->prepare( "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE parent = %d AND taxonomy = %s", $term_id, $term_taxonomy->taxonomy ) ); if ( ! empty( $children_tt_ids ) ) { foreach ( $children_tt_ids as $child_tt_id ) { $wpdb->update( $wpdb->term_taxonomy, array( 'parent' => $new_term_id ), array( 'term_taxonomy_id' => $child_tt_id ) ); clean_term_cache( (int) $child_tt_id, '', false ); } } else { // If the term has no children, we must force its taxonomy cache to be rebuilt separately. clean_term_cache( $new_term_id, $term_taxonomy->taxonomy, false ); } clean_term_cache( $term_id, $term_taxonomy->taxonomy, false ); /* * Taxonomy cache clearing is delayed to avoid race conditions that may occur when * regenerating the taxonomy's hierarchy tree. */ $taxonomies_to_clean = array( $term_taxonomy->taxonomy ); // Clean the cache for term taxonomies formerly shared with the current term. $shared_term_taxonomies = $wpdb->get_col( $wpdb->prepare( "SELECT taxonomy FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) ); $taxonomies_to_clean = array_merge( $taxonomies_to_clean, $shared_term_taxonomies ); foreach ( $taxonomies_to_clean as $taxonomy_to_clean ) { clean_taxonomy_cache( $taxonomy_to_clean ); } // Keep a record of term_ids that have been split, keyed by old term_id. See wp_get_split_term(). if ( $record ) { $split_term_data = get_option( '_split_terms', array() ); if ( ! isset( $split_term_data[ $term_id ] ) ) { $split_term_data[ $term_id ] = array(); } $split_term_data[ $term_id ][ $term_taxonomy->taxonomy ] = $new_term_id; update_option( '_split_terms', $split_term_data ); } // If we've just split the final shared term, set the "finished" flag. $shared_terms_exist = $wpdb->get_results( "SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id GROUP BY t.term_id HAVING term_tt_count > 1 LIMIT 1" ); if ( ! $shared_terms_exist ) { update_option( 'finished_splitting_shared_terms', true ); } /** * Fires after a previously shared taxonomy term is split into two separate terms. * * @since 4.2.0 * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ do_action( 'split_shared_term', $term_id, $new_term_id, $term_taxonomy_id, $term_taxonomy->taxonomy ); return $new_term_id; } /** * Splits a batch of shared taxonomy terms. * * @since 4.3.0 * * @global wpdb $wpdb WordPress database abstraction object. */ function _wp_batch_split_terms() { global $wpdb; $lock_name = 'term_split.lock'; // Try to lock. $lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", $lock_name, time() ) ); if ( ! $lock_result ) { $lock_result = get_option( $lock_name ); // Bail if we were unable to create a lock, or if the existing lock is still valid. if ( ! $lock_result || ( $lock_result > ( time() - HOUR_IN_SECONDS ) ) ) { wp_schedule_single_event( time() + ( 5 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' ); return; } } // Update the lock, as by this point we've definitely got a lock, just need to fire the actions. update_option( $lock_name, time() ); // Get a list of shared terms (those with more than one associated row in term_taxonomy). $shared_terms = $wpdb->get_results( "SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id GROUP BY t.term_id HAVING term_tt_count > 1 LIMIT 10" ); // No more terms, we're done here. if ( ! $shared_terms ) { update_option( 'finished_splitting_shared_terms', true ); delete_option( $lock_name ); return; } // Shared terms found? We'll need to run this script again. wp_schedule_single_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' ); // Rekey shared term array for faster lookups. $_shared_terms = array(); foreach ( $shared_terms as $shared_term ) { $term_id = (int) $shared_term->term_id; $_shared_terms[ $term_id ] = $shared_term; } $shared_terms = $_shared_terms; // Get term taxonomy data for all shared terms. $shared_term_ids = implode( ',', array_keys( $shared_terms ) ); $shared_tts = $wpdb->get_results( "SELECT * FROM {$wpdb->term_taxonomy} WHERE `term_id` IN ({$shared_term_ids})" ); // Split term data recording is slow, so we do it just once, outside the loop. $split_term_data = get_option( '_split_terms', array() ); $skipped_first_term = array(); $taxonomies = array(); foreach ( $shared_tts as $shared_tt ) { $term_id = (int) $shared_tt->term_id; // Don't split the first tt belonging to a given term_id. if ( ! isset( $skipped_first_term[ $term_id ] ) ) { $skipped_first_term[ $term_id ] = 1; continue; } if ( ! isset( $split_term_data[ $term_id ] ) ) { $split_term_data[ $term_id ] = array(); } // Keep track of taxonomies whose hierarchies need flushing. if ( ! isset( $taxonomies[ $shared_tt->taxonomy ] ) ) { $taxonomies[ $shared_tt->taxonomy ] = 1; } // Split the term. $split_term_data[ $term_id ][ $shared_tt->taxonomy ] = _split_shared_term( $shared_terms[ $term_id ], $shared_tt, false ); } // Rebuild the cached hierarchy for each affected taxonomy. foreach ( array_keys( $taxonomies ) as $tax ) { delete_option( "{$tax}_children" ); _get_term_hierarchy( $tax ); } update_option( '_split_terms', $split_term_data ); delete_option( $lock_name ); } /** * In order to avoid the _wp_batch_split_terms() job being accidentally removed, * checks that it's still scheduled while we haven't finished splitting terms. * * @ignore * @since 4.3.0 */ function _wp_check_for_scheduled_split_terms() { if ( ! get_option( 'finished_splitting_shared_terms' ) && ! wp_next_scheduled( 'wp_split_shared_term_batch' ) ) { wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'wp_split_shared_term_batch' ); } } /** * Checks default categories when a term gets split to see if any of them need to be updated. * * @ignore * @since 4.2.0 * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ function _wp_check_split_default_terms( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { if ( 'category' !== $taxonomy ) { return; } foreach ( array( 'default_category', 'default_link_category', 'default_email_category' ) as $option ) { if ( (int) get_option( $option, -1 ) === $term_id ) { update_option( $option, $new_term_id ); } } } /** * Checks menu items when a term gets split to see if any of them need to be updated. * * @ignore * @since 4.2.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ function _wp_check_split_terms_in_menus( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { global $wpdb; $post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT m1.post_id FROM {$wpdb->postmeta} AS m1 INNER JOIN {$wpdb->postmeta} AS m2 ON ( m2.post_id = m1.post_id ) INNER JOIN {$wpdb->postmeta} AS m3 ON ( m3.post_id = m1.post_id ) WHERE ( m1.meta_key = '_menu_item_type' AND m1.meta_value = 'taxonomy' ) AND ( m2.meta_key = '_menu_item_object' AND m2.meta_value = %s ) AND ( m3.meta_key = '_menu_item_object_id' AND m3.meta_value = %d )", $taxonomy, $term_id ) ); if ( $post_ids ) { foreach ( $post_ids as $post_id ) { update_post_meta( $post_id, '_menu_item_object_id', $new_term_id, $term_id ); } } } /** * If the term being split is a nav_menu, changes associations. * * @ignore * @since 4.3.0 * * @param int $term_id ID of the formerly shared term. * @param int $new_term_id ID of the new term created for the $term_taxonomy_id. * @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split. * @param string $taxonomy Taxonomy for the split term. */ function _wp_check_split_nav_menu_terms( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { if ( 'nav_menu' !== $taxonomy ) { return; } // Update menu locations. $locations = get_nav_menu_locations(); foreach ( $locations as $location => $menu_id ) { if ( $term_id === $menu_id ) { $locations[ $location ] = $new_term_id; } } set_theme_mod( 'nav_menu_locations', $locations ); } /** * Gets data about terms that previously shared a single term_id, but have since been split. * * @since 4.2.0 * * @param int $old_term_id Term ID. This is the old, pre-split term ID. * @return array Array of new term IDs, keyed by taxonomy. */ function wp_get_split_terms( $old_term_id ) { $split_terms = get_option( '_split_terms', array() ); $terms = array(); if ( isset( $split_terms[ $old_term_id ] ) ) { $terms = $split_terms[ $old_term_id ]; } return $terms; } /** * Gets the new term ID corresponding to a previously split term. * * @since 4.2.0 * * @param int $old_term_id Term ID. This is the old, pre-split term ID. * @param string $taxonomy Taxonomy that the term belongs to. * @return int|false If a previously split term is found corresponding to the old term_id and taxonomy, * the new term_id will be returned. If no previously split term is found matching * the parameters, returns false. */ function wp_get_split_term( $old_term_id, $taxonomy ) { $split_terms = wp_get_split_terms( $old_term_id ); $term_id = false; if ( isset( $split_terms[ $taxonomy ] ) ) { $term_id = (int) $split_terms[ $taxonomy ]; } return $term_id; } /** * Determines whether a term is shared between multiple taxonomies. * * Shared taxonomy terms began to be split in 4.3, but failed cron tasks or * other delays in upgrade routines may cause shared terms to remain. * * @since 4.4.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id Term ID. * @return bool Returns false if a term is not shared between multiple taxonomies or * if splitting shared taxonomy terms is finished. */ function wp_term_is_shared( $term_id ) { global $wpdb; if ( get_option( 'finished_splitting_shared_terms' ) ) { return false; } $tt_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) ); return $tt_count > 1; } /** * Generates a permalink for a taxonomy term archive. * * @since 2.5.0 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param WP_Term|int|string $term The term object, ID, or slug whose link will be retrieved. * @param string $taxonomy Optional. Taxonomy. Default empty. * @return string|WP_Error URL of the taxonomy term archive on success, WP_Error if term does not exist. */ function get_term_link( $term, $taxonomy = '' ) { global $wp_rewrite; if ( ! is_object( $term ) ) { if ( is_int( $term ) ) { $term = get_term( $term, $taxonomy ); } else { $term = get_term_by( 'slug', $term, $taxonomy ); } } if ( ! is_object( $term ) ) { $term = new WP_Error( 'invalid_term', __( 'Empty Term.' ) ); } if ( is_wp_error( $term ) ) { return $term; } $taxonomy = $term->taxonomy; $termlink = $wp_rewrite->get_extra_permastruct( $taxonomy ); /** * Filters the permalink structure for a term before token replacement occurs. * * @since 4.9.0 * * @param string $termlink The permalink structure for the term's taxonomy. * @param WP_Term $term The term object. */ $termlink = apply_filters( 'pre_term_link', $termlink, $term ); $slug = $term->slug; $t = get_taxonomy( $taxonomy ); if ( empty( $termlink ) ) { if ( 'category' === $taxonomy ) { $termlink = '?cat=' . $term->term_id; } elseif ( $t->query_var ) { $termlink = "?$t->query_var=$slug"; } else { $termlink = "?taxonomy=$taxonomy&term=$slug"; } $termlink = home_url( $termlink ); } else { if ( ! empty( $t->rewrite['hierarchical'] ) ) { $hierarchical_slugs = array(); $ancestors = get_ancestors( $term->term_id, $taxonomy, 'taxonomy' ); foreach ( (array) $ancestors as $ancestor ) { $ancestor_term = get_term( $ancestor, $taxonomy ); $hierarchical_slugs[] = $ancestor_term->slug; } $hierarchical_slugs = array_reverse( $hierarchical_slugs ); $hierarchical_slugs[] = $slug; $termlink = str_replace( "%$taxonomy%", implode( '/', $hierarchical_slugs ), $termlink ); } else { $termlink = str_replace( "%$taxonomy%", $slug, $termlink ); } $termlink = home_url( user_trailingslashit( $termlink, 'category' ) ); } // Back compat filters. if ( 'post_tag' === $taxonomy ) { /** * Filters the tag link. * * @since 2.3.0 * @since 2.5.0 Deprecated in favor of {@see 'term_link'} filter. * @since 5.4.1 Restored (un-deprecated). * * @param string $termlink Tag link URL. * @param int $term_id Term ID. */ $termlink = apply_filters( 'tag_link', $termlink, $term->term_id ); } elseif ( 'category' === $taxonomy ) { /** * Filters the category link. * * @since 1.5.0 * @since 2.5.0 Deprecated in favor of {@see 'term_link'} filter. * @since 5.4.1 Restored (un-deprecated). * * @param string $termlink Category link URL. * @param int $term_id Term ID. */ $termlink = apply_filters( 'category_link', $termlink, $term->term_id ); } /** * Filters the term link. * * @since 2.5.0 * * @param string $termlink Term link URL. * @param WP_Term $term Term object. * @param string $taxonomy Taxonomy slug. */ return apply_filters( 'term_link', $termlink, $term, $taxonomy ); } /** * Displays the taxonomies of a post with available options. * * This function can be used within the loop to display the taxonomies for a * post without specifying the Post ID. You can also use it outside the Loop to * display the taxonomies for a specific post. * * @since 2.5.0 * * @param array $args { * Arguments about which post to use and how to format the output. Shares all of the arguments * supported by get_the_taxonomies(), in addition to the following. * * @type int|WP_Post $post Post ID or object to get taxonomies of. Default current post. * @type string $before Displays before the taxonomies. Default empty string. * @type string $sep Separates each taxonomy. Default is a space. * @type string $after Displays after the taxonomies. Default empty string. * } */ function the_taxonomies( $args = array() ) { $defaults = array( 'post' => 0, 'before' => '', 'sep' => ' ', 'after' => '', ); $parsed_args = wp_parse_args( $args, $defaults ); echo $parsed_args['before'] . implode( $parsed_args['sep'], get_the_taxonomies( $parsed_args['post'], $parsed_args ) ) . $parsed_args['after']; } /** * Retrieves all taxonomies associated with a post. * * This function can be used within the loop. It will also return an array of * the taxonomies with links to the taxonomy and name. * * @since 2.5.0 * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @param array $args { * Optional. Arguments about how to format the list of taxonomies. Default empty array. * * @type string $template Template for displaying a taxonomy label and list of terms. * Default is "Label: Terms." * @type string $term_template Template for displaying a single term in the list. Default is the term name * linked to its archive. * } * @return string[] List of taxonomies. */ function get_the_taxonomies( $post = 0, $args = array() ) { $post = get_post( $post ); $args = wp_parse_args( $args, array( /* translators: %s: Taxonomy label, %l: List of terms formatted as per $term_template. */ 'template' => __( '%s: %l.' ), 'term_template' => '%2$s', ) ); $taxonomies = array(); if ( ! $post ) { return $taxonomies; } foreach ( get_object_taxonomies( $post ) as $taxonomy ) { $t = (array) get_taxonomy( $taxonomy ); if ( empty( $t['label'] ) ) { $t['label'] = $taxonomy; } if ( empty( $t['args'] ) ) { $t['args'] = array(); } if ( empty( $t['template'] ) ) { $t['template'] = $args['template']; } if ( empty( $t['term_template'] ) ) { $t['term_template'] = $args['term_template']; } $terms = get_object_term_cache( $post->ID, $taxonomy ); if ( false === $terms ) { $terms = wp_get_object_terms( $post->ID, $taxonomy, $t['args'] ); } $links = array(); foreach ( $terms as $term ) { $links[] = wp_sprintf( $t['term_template'], esc_attr( get_term_link( $term ) ), $term->name ); } if ( $links ) { $taxonomies[ $taxonomy ] = wp_sprintf( $t['template'], $t['label'], $links, $terms ); } } return $taxonomies; } /** * Retrieves all taxonomy names for the given post. * * @since 2.5.0 * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @return string[] An array of all taxonomy names for the given post. */ function get_post_taxonomies( $post = 0 ) { $post = get_post( $post ); return get_object_taxonomies( $post ); } /** * Determines if the given object is associated with any of the given terms. * * The given terms are checked against the object's terms' term_ids, names and slugs. * Terms given as integers will only be checked against the object's terms' term_ids. * If no terms are given, determines if object is associated with any terms in the given taxonomy. * * @since 2.7.0 * * @param int $object_id ID of the object (post ID, link ID, ...). * @param string $taxonomy Single taxonomy name. * @param int|string|int[]|string[] $terms Optional. Term ID, name, slug, or array of such * to check against. Default null. * @return bool|WP_Error WP_Error on input error. */ function is_object_in_term( $object_id, $taxonomy, $terms = null ) { $object_id = (int) $object_id; if ( ! $object_id ) { return new WP_Error( 'invalid_object', __( 'Invalid object ID.' ) ); } $object_terms = get_object_term_cache( $object_id, $taxonomy ); if ( false === $object_terms ) { $object_terms = wp_get_object_terms( $object_id, $taxonomy, array( 'update_term_meta_cache' => false ) ); if ( is_wp_error( $object_terms ) ) { return $object_terms; } wp_cache_set( $object_id, wp_list_pluck( $object_terms, 'term_id' ), "{$taxonomy}_relationships" ); } if ( is_wp_error( $object_terms ) ) { return $object_terms; } if ( empty( $object_terms ) ) { return false; } if ( empty( $terms ) ) { return ( ! empty( $object_terms ) ); } $terms = (array) $terms; $ints = array_filter( $terms, 'is_int' ); if ( $ints ) { $strs = array_diff( $terms, $ints ); } else { $strs =& $terms; } foreach ( $object_terms as $object_term ) { // If term is an int, check against term_ids only. if ( $ints && in_array( $object_term->term_id, $ints, true ) ) { return true; } if ( $strs ) { // Only check numeric strings against term_id, to avoid false matches due to type juggling. $numeric_strs = array_map( 'intval', array_filter( $strs, 'is_numeric' ) ); if ( in_array( $object_term->term_id, $numeric_strs, true ) ) { return true; } if ( in_array( $object_term->name, $strs, true ) ) { return true; } if ( in_array( $object_term->slug, $strs, true ) ) { return true; } } } return false; } /** * Determines if the given object type is associated with the given taxonomy. * * @since 3.0.0 * * @param string $object_type Object type string. * @param string $taxonomy Single taxonomy name. * @return bool True if object is associated with the taxonomy, otherwise false. */ function is_object_in_taxonomy( $object_type, $taxonomy ) { $taxonomies = get_object_taxonomies( $object_type ); if ( empty( $taxonomies ) ) { return false; } return in_array( $taxonomy, $taxonomies, true ); } /** * Gets an array of ancestor IDs for a given object. * * @since 3.1.0 * @since 4.1.0 Introduced the `$resource_type` argument. * * @param int $object_id Optional. The ID of the object. Default 0. * @param string $object_type Optional. The type of object for which we'll be retrieving * ancestors. Accepts a post type or a taxonomy name. Default empty. * @param string $resource_type Optional. Type of resource $object_type is. Accepts 'post_type' * or 'taxonomy'. Default empty. * @return int[] An array of IDs of ancestors from lowest to highest in the hierarchy. */ function get_ancestors( $object_id = 0, $object_type = '', $resource_type = '' ) { $object_id = (int) $object_id; $ancestors = array(); if ( empty( $object_id ) ) { /** This filter is documented in wp-includes/taxonomy.php */ return apply_filters( 'get_ancestors', $ancestors, $object_id, $object_type, $resource_type ); } if ( ! $resource_type ) { if ( is_taxonomy_hierarchical( $object_type ) ) { $resource_type = 'taxonomy'; } elseif ( post_type_exists( $object_type ) ) { $resource_type = 'post_type'; } } if ( 'taxonomy' === $resource_type ) { $term = get_term( $object_id, $object_type ); while ( ! is_wp_error( $term ) && ! empty( $term->parent ) && ! in_array( $term->parent, $ancestors, true ) ) { $ancestors[] = (int) $term->parent; $term = get_term( $term->parent, $object_type ); } } elseif ( 'post_type' === $resource_type ) { $ancestors = get_post_ancestors( $object_id ); } /** * Filters a given object's ancestors. * * @since 3.1.0 * @since 4.1.1 Introduced the `$resource_type` parameter. * * @param int[] $ancestors An array of IDs of object ancestors. * @param int $object_id Object ID. * @param string $object_type Type of object. * @param string $resource_type Type of resource $object_type is. */ return apply_filters( 'get_ancestors', $ancestors, $object_id, $object_type, $resource_type ); } /** * Returns the term's parent's term ID. * * @since 3.1.0 * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy name. * @return int|false Parent term ID on success, false on failure. */ function wp_get_term_taxonomy_parent_id( $term_id, $taxonomy ) { $term = get_term( $term_id, $taxonomy ); if ( ! $term || is_wp_error( $term ) ) { return false; } return (int) $term->parent; } /** * Checks the given subset of the term hierarchy for hierarchy loops. * Prevents loops from forming and breaks those that it finds. * * Attached to the {@see 'wp_update_term_parent'} filter. * * @since 3.1.0 * * @param int $parent_term `term_id` of the parent for the term we're checking. * @param int $term_id The term we're checking. * @param string $taxonomy The taxonomy of the term we're checking. * @return int The new parent for the term. */ function wp_check_term_hierarchy_for_loops( $parent_term, $term_id, $taxonomy ) { // Nothing fancy here - bail. if ( ! $parent_term ) { return 0; } // Can't be its own parent. if ( $parent_term === $term_id ) { return 0; } // Now look for larger loops. $loop = wp_find_hierarchy_loop( 'wp_get_term_taxonomy_parent_id', $term_id, $parent_term, array( $taxonomy ) ); if ( ! $loop ) { return $parent_term; // No loop. } // Setting $parent_term to the given value causes a loop. if ( isset( $loop[ $term_id ] ) ) { return 0; } // There's a loop, but it doesn't contain $term_id. Break the loop. foreach ( array_keys( $loop ) as $loop_member ) { wp_update_term( $loop_member, $taxonomy, array( 'parent' => 0 ) ); } return $parent_term; } /** * Determines whether a taxonomy is considered "viewable". * * @since 5.1.0 * * @param string|WP_Taxonomy $taxonomy Taxonomy name or object. * @return bool Whether the taxonomy should be considered viewable. */ function is_taxonomy_viewable( $taxonomy ) { if ( is_scalar( $taxonomy ) ) { $taxonomy = get_taxonomy( $taxonomy ); if ( ! $taxonomy ) { return false; } } return $taxonomy->publicly_queryable; } /** * Determines whether a term is publicly viewable. * * A term is considered publicly viewable if its taxonomy is viewable. * * @since 6.1.0 * * @param int|WP_Term $term Term ID or term object. * @return bool Whether the term is publicly viewable. */ function is_term_publicly_viewable( $term ) { $term = get_term( $term ); if ( ! $term ) { return false; } return is_taxonomy_viewable( $term->taxonomy ); } /** * Sets the last changed time for the 'terms' cache group. * * @since 5.0.0 */ function wp_cache_set_terms_last_changed() { wp_cache_set_last_changed( 'terms' ); } /** * Aborts calls to term meta if it is not supported. * * @since 5.0.0 * * @param mixed $check Skip-value for whether to proceed term meta function execution. * @return mixed Original value of $check, or false if term meta is not supported. */ function wp_check_term_meta_support_prefilter( $check ) { if ( get_option( 'db_version' ) < 34370 ) { return false; } return $check; }